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:
Mayank
2020-04-03 08:01:34 -07:00
committed by GitHub
parent 061b4463ce
commit 016868ecd3
6 changed files with 419 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
Adding new restoreItemAction for PVC to update the selected-node annotation
+47
View File
@@ -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
}
+16
View File
@@ -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
}
}
+155
View File
@@ -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)
}
})
}
}
+28 -2
View File
@@ -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>