Create new ItemBlockAction (IBA) plugin type

Signed-off-by: Scott Seago <sseago@redhat.com>
This commit is contained in:
Scott Seago
2024-07-18 09:57:44 -04:00
parent d9ca147479
commit ba9c109868
21 changed files with 1851 additions and 14 deletions

View File

@@ -27,6 +27,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
biav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v1"
biav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2"
ibav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/itemblockaction/v1"
riav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/restoreitemaction/v1"
riav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/restoreitemaction/v2"
"github.com/vmware-tanzu/velero/pkg/util/collections"
@@ -280,3 +281,38 @@ func (recv DeleteItemActionResolver) ResolveActions(helper discovery.Helper, log
}
return resolved, nil
}
type ItemBlockResolvedAction struct {
ibav1.ItemBlockAction
resolvedAction
}
type ItemBlockActionResolver struct {
actions []ibav1.ItemBlockAction
}
func NewItemBlockActionResolver(actions []ibav1.ItemBlockAction) ItemBlockActionResolver {
return ItemBlockActionResolver{
actions: actions,
}
}
func (recv ItemBlockActionResolver) ResolveActions(helper discovery.Helper, log logrus.FieldLogger) ([]ItemBlockResolvedAction, error) {
var resolved []ItemBlockResolvedAction
for _, action := range recv.actions {
resources, namespaces, selector, err := resolveAction(helper, action)
if err != nil {
return nil, err
}
res := ItemBlockResolvedAction{
ItemBlockAction: action,
resolvedAction: resolvedAction{
ResourceIncludesExcludes: resources,
NamespaceIncludesExcludes: namespaces,
Selector: selector,
},
}
resolved = append(resolved, res)
}
return resolved, nil
}

View File

@@ -47,6 +47,9 @@ const (
// PluginKindDeleteItemAction represents a delete item action plugin.
PluginKindDeleteItemAction PluginKind = "DeleteItemAction"
// PluginKindItemBlockAction represents a v1 ItemBlock action plugin.
PluginKindItemBlockAction PluginKind = "ItemBlockAction"
// PluginKindPluginLister represents a plugin lister plugin.
PluginKindPluginLister PluginKind = "PluginLister"
)
@@ -70,5 +73,6 @@ func AllPluginKinds() map[string]PluginKind {
allPluginKinds[PluginKindRestoreItemAction.String()] = PluginKindRestoreItemAction
allPluginKinds[PluginKindRestoreItemActionV2.String()] = PluginKindRestoreItemActionV2
allPluginKinds[PluginKindDeleteItemAction.String()] = PluginKindDeleteItemAction
allPluginKinds[PluginKindItemBlockAction.String()] = PluginKindItemBlockAction
return allPluginKinds
}

View File

@@ -0,0 +1,45 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
plugin "github.com/hashicorp/go-plugin"
"golang.org/x/net/context"
"google.golang.org/grpc"
"github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
protoibav1 "github.com/vmware-tanzu/velero/pkg/plugin/generated/itemblockaction/v1"
)
// ItemBlockActionPlugin is an implementation of go-plugin's Plugin
// interface with support for gRPC for the backup/ItemAction
// interface.
type ItemBlockActionPlugin struct {
plugin.NetRPCUnsupportedPlugin
*common.PluginBase
}
// GRPCClient returns a clientDispenser for ItemBlockAction gRPC clients.
func (p *ItemBlockActionPlugin) GRPCClient(_ context.Context, _ *plugin.GRPCBroker, clientConn *grpc.ClientConn) (interface{}, error) {
return common.NewClientDispenser(p.ClientLogger, clientConn, newItemBlockActionGRPCClient), nil
}
// GRPCServer registers a ItemBlockAction gRPC server.
func (p *ItemBlockActionPlugin) GRPCServer(_ *plugin.GRPCBroker, server *grpc.Server) error {
protoibav1.RegisterItemBlockActionServer(server, &ItemBlockActionGRPCServer{mux: p.ServerMux})
return nil
}

View File

@@ -0,0 +1,122 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"encoding/json"
"github.com/pkg/errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
protoibav1 "github.com/vmware-tanzu/velero/pkg/plugin/generated/itemblockaction/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
)
// NewItemBlockActionPlugin constructs a ItemBlockActionPlugin.
func NewItemBlockActionPlugin(options ...common.PluginOption) *ItemBlockActionPlugin {
return &ItemBlockActionPlugin{
PluginBase: common.NewPluginBase(options...),
}
}
// ItemBlockActionGRPCClient implements the backup/ItemAction interface and uses a
// gRPC client to make calls to the plugin server.
type ItemBlockActionGRPCClient struct {
*common.ClientBase
grpcClient protoibav1.ItemBlockActionClient
}
func newItemBlockActionGRPCClient(base *common.ClientBase, clientConn *grpc.ClientConn) interface{} {
return &ItemBlockActionGRPCClient{
ClientBase: base,
grpcClient: protoibav1.NewItemBlockActionClient(clientConn),
}
}
func (c *ItemBlockActionGRPCClient) AppliesTo() (velero.ResourceSelector, error) {
req := &protoibav1.ItemBlockActionAppliesToRequest{
Plugin: c.Plugin,
}
res, err := c.grpcClient.AppliesTo(context.Background(), req)
if err != nil {
return velero.ResourceSelector{}, common.FromGRPCError(err)
}
if res.ResourceSelector == nil {
return velero.ResourceSelector{}, nil
}
return velero.ResourceSelector{
IncludedNamespaces: res.ResourceSelector.IncludedNamespaces,
ExcludedNamespaces: res.ResourceSelector.ExcludedNamespaces,
IncludedResources: res.ResourceSelector.IncludedResources,
ExcludedResources: res.ResourceSelector.ExcludedResources,
LabelSelector: res.ResourceSelector.Selector,
}, nil
}
func (c *ItemBlockActionGRPCClient) GetRelatedItems(item runtime.Unstructured, backup *api.Backup) ([]velero.ResourceIdentifier, error) {
itemJSON, err := json.Marshal(item.UnstructuredContent())
if err != nil {
return nil, errors.WithStack(err)
}
backupJSON, err := json.Marshal(backup)
if err != nil {
return nil, errors.WithStack(err)
}
req := &protoibav1.ItemBlockActionGetRelatedItemsRequest{
Plugin: c.Plugin,
Item: itemJSON,
Backup: backupJSON,
}
res, err := c.grpcClient.GetRelatedItems(context.Background(), req)
if err != nil {
return nil, common.FromGRPCError(err)
}
var relatedItems []velero.ResourceIdentifier
for _, itm := range res.RelatedItems {
newItem := velero.ResourceIdentifier{
GroupResource: schema.GroupResource{
Group: itm.Group,
Resource: itm.Resource,
},
Namespace: itm.Namespace,
Name: itm.Name,
}
relatedItems = append(relatedItems, newItem)
}
return relatedItems, nil
}
// This shouldn't be called on the GRPC client since the RestartableItemBlockAction won't delegate
// this method
func (c *ItemBlockActionGRPCClient) Name() string {
return ""
}

View File

@@ -0,0 +1,134 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"encoding/json"
"github.com/pkg/errors"
"golang.org/x/net/context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
proto "github.com/vmware-tanzu/velero/pkg/plugin/generated"
protoibav1 "github.com/vmware-tanzu/velero/pkg/plugin/generated/itemblockaction/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
ibav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/itemblockaction/v1"
)
// ItemBlockActionGRPCServer implements the proto-generated ItemBlockAction interface, and accepts
// gRPC calls and forwards them to an implementation of the pluggable interface.
type ItemBlockActionGRPCServer struct {
mux *common.ServerMux
}
func (s *ItemBlockActionGRPCServer) getImpl(name string) (ibav1.ItemBlockAction, error) {
impl, err := s.mux.GetHandler(name)
if err != nil {
return nil, err
}
itemAction, ok := impl.(ibav1.ItemBlockAction)
if !ok {
return nil, errors.Errorf("%T is not a backup item action", impl)
}
return itemAction, nil
}
func (s *ItemBlockActionGRPCServer) AppliesTo(
ctx context.Context, req *protoibav1.ItemBlockActionAppliesToRequest) (
response *protoibav1.ItemBlockActionAppliesToResponse, err error) {
defer func() {
if recoveredErr := common.HandlePanic(recover()); recoveredErr != nil {
err = recoveredErr
}
}()
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, common.NewGRPCError(err)
}
resourceSelector, err := impl.AppliesTo()
if err != nil {
return nil, common.NewGRPCError(err)
}
return &protoibav1.ItemBlockActionAppliesToResponse{
ResourceSelector: &proto.ResourceSelector{
IncludedNamespaces: resourceSelector.IncludedNamespaces,
ExcludedNamespaces: resourceSelector.ExcludedNamespaces,
IncludedResources: resourceSelector.IncludedResources,
ExcludedResources: resourceSelector.ExcludedResources,
Selector: resourceSelector.LabelSelector,
},
}, nil
}
func (s *ItemBlockActionGRPCServer) GetRelatedItems(
ctx context.Context, req *protoibav1.ItemBlockActionGetRelatedItemsRequest) (response *protoibav1.ItemBlockActionGetRelatedItemsResponse, err error) {
defer func() {
if recoveredErr := common.HandlePanic(recover()); recoveredErr != nil {
err = recoveredErr
}
}()
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, common.NewGRPCError(err)
}
var item unstructured.Unstructured
var backup api.Backup
if err := json.Unmarshal(req.Item, &item); err != nil {
return nil, common.NewGRPCError(errors.WithStack(err))
}
if err := json.Unmarshal(req.Backup, &backup); err != nil {
return nil, common.NewGRPCError(errors.WithStack(err))
}
relatedItems, err := impl.GetRelatedItems(&item, &backup)
if err != nil {
return nil, common.NewGRPCError(err)
}
res := &protoibav1.ItemBlockActionGetRelatedItemsResponse{}
for _, item := range relatedItems {
res.RelatedItems = append(res.RelatedItems, backupResourceIdentifierToProto(item))
}
return res, nil
}
func backupResourceIdentifierToProto(id velero.ResourceIdentifier) *proto.ResourceIdentifier {
return &proto.ResourceIdentifier{
Group: id.Group,
Resource: id.Resource,
Namespace: id.Namespace,
Name: id.Name,
}
}
// This shouldn't be called on the GRPC server since the server won't ever receive this request, as
// the RestartableItemBlockAction in Velero won't delegate this to the server
func (s *ItemBlockActionGRPCServer) Name() string {
return ""
}

View File

@@ -0,0 +1,163 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"encoding/json"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
proto "github.com/vmware-tanzu/velero/pkg/plugin/generated"
protoibav1 "github.com/vmware-tanzu/velero/pkg/plugin/generated/itemblockaction/v1"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
mocks "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks/itemblockaction/v1"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
func TestItemBlockActionGRPCServerGetRelatedItems(t *testing.T) {
invalidItem := []byte("this is gibberish json")
validItem := []byte(`
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"namespace": "myns",
"name": "myconfigmap"
},
"data": {
"key": "value"
}
}`)
var validItemObject unstructured.Unstructured
err := json.Unmarshal(validItem, &validItemObject)
require.NoError(t, err)
invalidBackup := []byte("this is gibberish json")
validBackup := []byte(`
{
"apiVersion": "velero.io/v1",
"kind": "Backup",
"metadata": {
"namespace": "myns",
"name": "mybackup"
},
"spec": {
"includedNamespaces": ["*"],
"includedResources": ["*"],
"ttl": "60m"
}
}`)
var validBackupObject v1.Backup
err = json.Unmarshal(validBackup, &validBackupObject)
require.NoError(t, err)
tests := []struct {
name string
backup []byte
item []byte
implRelatedItems []velero.ResourceIdentifier
implError error
expectError bool
skipMock bool
}{
{
name: "error unmarshaling item",
item: invalidItem,
backup: validBackup,
expectError: true,
skipMock: true,
},
{
name: "error unmarshaling backup",
item: validItem,
backup: invalidBackup,
expectError: true,
skipMock: true,
},
{
name: "error running impl",
item: validItem,
backup: validBackup,
implError: errors.New("impl error"),
expectError: true,
},
{
name: "no relatedItems",
item: validItem,
backup: validBackup,
},
{
name: "some relatedItems",
item: validItem,
backup: validBackup,
implRelatedItems: []velero.ResourceIdentifier{
{
GroupResource: schema.GroupResource{Group: "v1", Resource: "pods"},
Namespace: "myns",
Name: "mypod",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
itemAction := &mocks.ItemBlockAction{}
defer itemAction.AssertExpectations(t)
if !test.skipMock {
itemAction.On("GetRelatedItems", &validItemObject, &validBackupObject).Return(test.implRelatedItems, test.implError)
}
s := &ItemBlockActionGRPCServer{mux: &common.ServerMux{
ServerLog: velerotest.NewLogger(),
Handlers: map[string]interface{}{
"xyz": itemAction,
},
}}
req := &protoibav1.ItemBlockActionGetRelatedItemsRequest{
Plugin: "xyz",
Item: test.item,
Backup: test.backup,
}
resp, err := s.GetRelatedItems(context.Background(), req)
// Verify error
assert.Equal(t, test.expectError, err != nil)
if err != nil {
return
}
require.NotNil(t, resp)
// Verify related items
var expectedRelatedItems []*proto.ResourceIdentifier
for _, item := range test.implRelatedItems {
expectedRelatedItems = append(expectedRelatedItems, backupResourceIdentifierToProto(item))
}
assert.Equal(t, expectedRelatedItems, resp.RelatedItems)
})
}
}

View File

@@ -27,6 +27,7 @@ import (
biav2 "github.com/vmware-tanzu/velero/pkg/plugin/framework/backupitemaction/v2"
"github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
ibav1 "github.com/vmware-tanzu/velero/pkg/plugin/framework/itemblockaction/v1"
riav2 "github.com/vmware-tanzu/velero/pkg/plugin/framework/restoreitemaction/v2"
"github.com/vmware-tanzu/velero/pkg/util/logging"
)
@@ -90,6 +91,13 @@ type Server interface {
// RegisterDeleteItemActions registers multiple Delete item actions.
RegisterDeleteItemActions(map[string]common.HandlerInitializer) Server
// RegisterItemBlockAction registers a ItemBlock action. Accepted format
// for the plugin name is <DNS subdomain>/<non-empty name>.
RegisterItemBlockAction(pluginName string, initializer common.HandlerInitializer) Server
// RegisterItemBlockActions registers multiple ItemBlock actions.
RegisterItemBlockActions(map[string]common.HandlerInitializer) Server
// Server runs the plugin server.
Serve()
}
@@ -106,6 +114,7 @@ type server struct {
restoreItemAction *RestoreItemActionPlugin
restoreItemActionV2 *riav2.RestoreItemActionPlugin
deleteItemAction *DeleteItemActionPlugin
itemBlockAction *ibav1.ItemBlockActionPlugin
}
// NewServer returns a new Server
@@ -122,6 +131,7 @@ func NewServer() Server {
restoreItemAction: NewRestoreItemActionPlugin(common.ServerLogger(log)),
restoreItemActionV2: riav2.NewRestoreItemActionPlugin(common.ServerLogger(log)),
deleteItemAction: NewDeleteItemActionPlugin(common.ServerLogger(log)),
itemBlockAction: ibav1.NewItemBlockActionPlugin(common.ServerLogger(log)),
}
}
@@ -217,6 +227,18 @@ func (s *server) RegisterDeleteItemActions(m map[string]common.HandlerInitialize
return s
}
func (s *server) RegisterItemBlockAction(name string, initializer common.HandlerInitializer) Server {
s.itemBlockAction.Register(name, initializer)
return s
}
func (s *server) RegisterItemBlockActions(m map[string]common.HandlerInitializer) Server {
for name := range m {
s.RegisterItemBlockAction(name, m[name])
}
return s
}
// getNames returns a list of PluginIdentifiers registered with plugin.
func getNames(command string, kind common.PluginKind, plugin Interface) []PluginIdentifier {
var pluginIdentifiers []PluginIdentifier
@@ -251,6 +273,7 @@ func (s *server) Serve() {
pluginIdentifiers = append(pluginIdentifiers, getNames(command, common.PluginKindRestoreItemAction, s.restoreItemAction)...)
pluginIdentifiers = append(pluginIdentifiers, getNames(command, common.PluginKindRestoreItemActionV2, s.restoreItemActionV2)...)
pluginIdentifiers = append(pluginIdentifiers, getNames(command, common.PluginKindDeleteItemAction, s.deleteItemAction)...)
pluginIdentifiers = append(pluginIdentifiers, getNames(command, common.PluginKindItemBlockAction, s.itemBlockAction)...)
pluginLister := NewPluginLister(pluginIdentifiers...)
@@ -265,6 +288,7 @@ func (s *server) Serve() {
string(common.PluginKindRestoreItemAction): s.restoreItemAction,
string(common.PluginKindRestoreItemActionV2): s.restoreItemActionV2,
string(common.PluginKindDeleteItemAction): s.deleteItemAction,
string(common.PluginKindItemBlockAction): s.itemBlockAction,
},
GRPCServer: plugin.DefaultGRPCServer,
})