mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-08-15 11:46:06 +00:00
add restore item action to update PVC selected-node annotation (#2377)
* New RestoreItemAction for PVC with annotation `volume.kubernetes.io/selected-node` Signed-off-by: mayank <mayank.patel@mayadata.io>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Adding new restoreItemAction for PVC to update the selected-node annotation
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2020 the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// NodeBuilder builds Node objects.
|
||||
type NodeBuilder struct {
|
||||
object *corev1api.Node
|
||||
}
|
||||
|
||||
// ForNode is the constructor for a NodeBuilder.
|
||||
func ForNode(name string) *NodeBuilder {
|
||||
return &NodeBuilder{
|
||||
object: &corev1api.Node{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: corev1api.SchemeGroupVersion.String(),
|
||||
Kind: "Node",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Result returns the built Node.
|
||||
func (b *NodeBuilder) Result() *corev1api.Node {
|
||||
return b.object
|
||||
}
|
||||
@@ -50,6 +50,7 @@ func NewCommand(f client.Factory) *cobra.Command {
|
||||
RegisterRestoreItemAction("velero.io/role-bindings", newRoleBindingItemAction).
|
||||
RegisterRestoreItemAction("velero.io/cluster-role-bindings", newClusterRoleBindingItemAction).
|
||||
RegisterRestoreItemAction("velero.io/crd-preserve-fields", newCRDV1PreserveUnknownFieldsItemAction).
|
||||
RegisterRestoreItemAction("velero.io/change-pvc-node-selector", newChangePVCNodeSelectorItemAction(f)).
|
||||
Serve()
|
||||
},
|
||||
}
|
||||
@@ -162,3 +163,18 @@ func newRoleBindingItemAction(logger logrus.FieldLogger) (interface{}, error) {
|
||||
func newClusterRoleBindingItemAction(logger logrus.FieldLogger) (interface{}, error) {
|
||||
return restore.NewClusterRoleBindingAction(logger), nil
|
||||
}
|
||||
|
||||
func newChangePVCNodeSelectorItemAction(f client.Factory) veleroplugin.HandlerInitializer {
|
||||
return func(logger logrus.FieldLogger) (interface{}, error) {
|
||||
client, err := f.KubeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return restore.NewChangePVCNodeSelectorAction(
|
||||
logger,
|
||||
client.CoreV1().ConfigMaps(f.Namespace()),
|
||||
client.CoreV1().Nodes(),
|
||||
), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Copyright 2020 the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
)
|
||||
|
||||
// ChangePVCNodeSelectorAction updates/reset PVC's node selector
|
||||
// if a mapping is found in the plugin's config map.
|
||||
type ChangePVCNodeSelectorAction struct {
|
||||
logger logrus.FieldLogger
|
||||
configMapClient corev1client.ConfigMapInterface
|
||||
nodeClient corev1client.NodeInterface
|
||||
}
|
||||
|
||||
// NewChangePVCNodeSelectorAction is the constructor for ChangePVCNodeSelectorAction.
|
||||
func NewChangePVCNodeSelectorAction(
|
||||
logger logrus.FieldLogger,
|
||||
configMapClient corev1client.ConfigMapInterface,
|
||||
nodeClient corev1client.NodeInterface,
|
||||
) *ChangePVCNodeSelectorAction {
|
||||
return &ChangePVCNodeSelectorAction{
|
||||
logger: logger,
|
||||
configMapClient: configMapClient,
|
||||
nodeClient: nodeClient,
|
||||
}
|
||||
}
|
||||
|
||||
// AppliesTo returns the resources that ChangePVCNodeSelectorAction should be run for
|
||||
func (p *ChangePVCNodeSelectorAction) AppliesTo() (velero.ResourceSelector, error) {
|
||||
return velero.ResourceSelector{
|
||||
IncludedResources: []string{"persistentvolumeclaims"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute updates the pvc's selected-node annotation:
|
||||
// a) if node mapping found in the config map for the plugin
|
||||
// b) if node mentioned in annotation doesn't exist
|
||||
func (p *ChangePVCNodeSelectorAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
p.logger.Info("Executing ChangePVCNodeSelectorAction")
|
||||
defer p.logger.Info("Done executing ChangePVCNodeSelectorAction")
|
||||
|
||||
typeAcc, err := meta.TypeAccessor(input.Item)
|
||||
if err != nil {
|
||||
return &velero.RestoreItemActionExecuteOutput{}, err
|
||||
}
|
||||
|
||||
metadata, err := meta.Accessor(input.Item)
|
||||
if err != nil {
|
||||
return &velero.RestoreItemActionExecuteOutput{}, err
|
||||
}
|
||||
|
||||
annotations := metadata.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return velero.NewRestoreItemActionExecuteOutput(input.Item), nil
|
||||
}
|
||||
|
||||
log := p.logger.WithFields(map[string]interface{}{
|
||||
"kind": typeAcc.GetKind(),
|
||||
"namespace": metadata.GetNamespace(),
|
||||
"name": metadata.GetName(),
|
||||
})
|
||||
|
||||
// let's check if PVC has annotation of the selected node
|
||||
node, ok := annotations["volume.kubernetes.io/selected-node"]
|
||||
if !ok {
|
||||
log.Debug("PVC doesn't have node selector")
|
||||
return velero.NewRestoreItemActionExecuteOutput(input.Item), nil
|
||||
}
|
||||
|
||||
// fetch node mapping from configMap
|
||||
newNode, err := getNewNodeFromConfigMap(p.configMapClient, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(newNode) != 0 {
|
||||
// set node selector
|
||||
// We assume that node exist for node-mapping
|
||||
annotations["volume.kubernetes.io/selected-node"] = newNode
|
||||
metadata.SetAnnotations(annotations)
|
||||
log.Infof("Updating selected-node to %s from %s", newNode, node)
|
||||
return velero.NewRestoreItemActionExecuteOutput(input.Item), nil
|
||||
}
|
||||
|
||||
// configMap doesn't have node-mapping
|
||||
// Let's check if node exists or not
|
||||
exists, err := isNodeExist(p.nodeClient, node)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error checking node %s existence", node)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
log.Infof("Clearing selected-node because node named %s does not exist", node)
|
||||
delete(annotations, "volume.kubernetes.io/selected-node")
|
||||
if len(annotations) == 0 {
|
||||
metadata.SetAnnotations(nil)
|
||||
} else {
|
||||
metadata.SetAnnotations(annotations)
|
||||
}
|
||||
}
|
||||
|
||||
return velero.NewRestoreItemActionExecuteOutput(input.Item), nil
|
||||
}
|
||||
|
||||
func getNewNodeFromConfigMap(client corev1client.ConfigMapInterface, node string) (string, error) {
|
||||
// fetch node mapping from configMap
|
||||
config, err := getPluginConfig(framework.PluginKindRestoreItemAction, "velero.io/change-pvc-node", client)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
// there is no node mapping defined for change-pvc-node
|
||||
// so we will return empty new node
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return config.Data[node], nil
|
||||
}
|
||||
|
||||
// isNodeExist check if node resource exist or not
|
||||
func isNodeExist(nodeClient corev1client.NodeInterface, name string) (bool, error) {
|
||||
_, err := nodeClient.Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
Copyright 2020 the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
)
|
||||
|
||||
// TestChangePVCNodeSelectorActionExecute runs the ChangePVCNodeSelectorAction's Execute
|
||||
// method and validates that the item's PVC is modified (or not) as expected.
|
||||
// Validation is done by comparing the result of the Execute method to the test case's
|
||||
// desired result.
|
||||
func TestChangePVCNodeSelectorActionExecute(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pvc *corev1api.PersistentVolumeClaim
|
||||
configMap *corev1api.ConfigMap
|
||||
node *corev1api.Node
|
||||
want *corev1api.PersistentVolumeClaim
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "a valid mapping for a persistent volume claim is applied correctly",
|
||||
pvc: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").
|
||||
ObjectMeta(
|
||||
builder.WithAnnotations("volume.kubernetes.io/selected-node", "source-node"),
|
||||
).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-pvc-node").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-pvc-node", "RestoreItemAction")).
|
||||
Data("source-node", "dest-node").
|
||||
Result(),
|
||||
want: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").
|
||||
ObjectMeta(
|
||||
builder.WithAnnotations("volume.kubernetes.io/selected-node", "dest-node"),
|
||||
).Result(),
|
||||
},
|
||||
{
|
||||
name: "when no config map exists for the plugin and node doesn't exist, the item is returned without node selector",
|
||||
pvc: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").
|
||||
ObjectMeta(
|
||||
builder.WithAnnotations("volume.kubernetes.io/selected-node", "source-node"),
|
||||
).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-pvc-node").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/some-other-plugin", "RestoreItemAction")).
|
||||
Data("source-noed", "dest-node").
|
||||
Result(),
|
||||
want: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").Result(),
|
||||
},
|
||||
{
|
||||
name: "when no node-mappings exist in the plugin config map and selected-node doesn't exist, the item is returned without node selector",
|
||||
pvc: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").
|
||||
ObjectMeta(
|
||||
builder.WithAnnotations("volume.kubernetes.io/selected-node", "source-node"),
|
||||
).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-pvc-node").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-pvc-node", "RestoreItemAction")).
|
||||
Result(),
|
||||
want: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").Result(),
|
||||
},
|
||||
{
|
||||
name: "when no node-mappings exist in the plugin config map and selected-node exist, the item is returned as-is",
|
||||
pvc: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").
|
||||
ObjectMeta(
|
||||
builder.WithAnnotations("volume.kubernetes.io/selected-node", "source-node"),
|
||||
).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-pvc-node").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-pvc-node", "RestoreItemAction")).
|
||||
Result(),
|
||||
// MAYANK TODO
|
||||
node: builder.ForNode("source-node").Result(),
|
||||
want: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").
|
||||
ObjectMeta(
|
||||
builder.WithAnnotations("volume.kubernetes.io/selected-node", "source-node"),
|
||||
).Result(),
|
||||
},
|
||||
{
|
||||
name: "when persistent volume claim has no node selector, the item is returned as-is",
|
||||
pvc: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-pvc-node").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-pvc-node", "RestoreItemAction")).
|
||||
Data("source-node", "dest-node").
|
||||
Result(),
|
||||
want: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").Result(),
|
||||
},
|
||||
{
|
||||
name: "when persistent volume claim's node-selector has no mapping in the config map, the item is returned without node selector",
|
||||
pvc: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").
|
||||
ObjectMeta(
|
||||
builder.WithAnnotations("volume.kubernetes.io/selected-node", "source-node"),
|
||||
).Result(),
|
||||
configMap: builder.ForConfigMap("velero", "change-pvc-node").
|
||||
ObjectMeta(builder.WithLabels("velero.io/plugin-config", "true", "velero.io/change-pvc-node", "RestoreItemAction")).
|
||||
Data("source-node-1", "dest-node").
|
||||
Result(),
|
||||
want: builder.ForPersistentVolumeClaim("source-ns", "pvc-1").Result(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset()
|
||||
a := NewChangePVCNodeSelectorAction(
|
||||
logrus.StandardLogger(),
|
||||
clientset.CoreV1().ConfigMaps("velero"),
|
||||
clientset.CoreV1().Nodes(),
|
||||
)
|
||||
|
||||
// set up test data
|
||||
if tc.configMap != nil {
|
||||
_, err := clientset.CoreV1().ConfigMaps(tc.configMap.Namespace).Create(tc.configMap)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if tc.node != nil {
|
||||
_, err := clientset.CoreV1().Nodes().Create(tc.node)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pvc)
|
||||
require.NoError(t, err)
|
||||
|
||||
input := &velero.RestoreItemActionExecuteInput{
|
||||
Item: &unstructured.Unstructured{
|
||||
Object: unstructuredMap,
|
||||
},
|
||||
}
|
||||
|
||||
// execute method under test
|
||||
res, err := a.Execute(input)
|
||||
|
||||
// validate for both error and non-error cases
|
||||
switch {
|
||||
case tc.wantErr != nil:
|
||||
assert.EqualError(t, err, tc.wantErr.Error())
|
||||
default:
|
||||
fmt.Printf("got +%v\n", res.UpdatedItem)
|
||||
assert.NoError(t, err)
|
||||
|
||||
wantUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.want)
|
||||
fmt.Printf("exptected +%v\n", wantUnstructured)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, &unstructured.Unstructured{Object: wantUnstructured}, res.UpdatedItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -50,8 +50,7 @@ metadata:
|
||||
# ConfigMap.
|
||||
labels:
|
||||
# this value-less label identifies the ConfigMap as
|
||||
# config for a plugin (i.e. the built-in change storage
|
||||
# class restore item action plugin)
|
||||
# config for a plugin (i.e. the built-in restore item action plugin)
|
||||
velero.io/plugin-config: ""
|
||||
# this label identifies the name and kind of plugin
|
||||
# that this ConfigMap is for.
|
||||
@@ -62,3 +61,30 @@ data:
|
||||
# class name.
|
||||
<old-storage-class>: <new-storage-class>
|
||||
```
|
||||
|
||||
## Changing PVC selected-node
|
||||
|
||||
Velero can update the selected-node annotation of persistent volume claim during restores, if selected-node doesn't exist in the cluster then it will remove the selected-node annotation from PersistentVolumeClaim. To configure a node mapping, create a config map in the Velero namespace like the following:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
# any name can be used; Velero uses the labels (below)
|
||||
# to identify it rather than the name
|
||||
name: change-pvc-node-selector-config
|
||||
# must be in the velero namespace
|
||||
namespace: velero
|
||||
# the below labels should be used verbatim in your
|
||||
# ConfigMap.
|
||||
labels:
|
||||
# this value-less label identifies the ConfigMap as
|
||||
# config for a plugin (i.e. the built-in restore item action plugin)
|
||||
velero.io/plugin-config: ""
|
||||
# this label identifies the name and kind of plugin
|
||||
# that this ConfigMap is for.
|
||||
velero.io/change-pvc-node-selector: RestoreItemAction
|
||||
data:
|
||||
# add 1+ key-value pairs here, where the key is the old
|
||||
# node name and the value is the new node name.
|
||||
<old-node-name>: <new-node-name>
|
||||
|
||||
Reference in New Issue
Block a user