diff --git a/pkg/backup/mocks/item_action.go b/pkg/backup/mocks/item_action.go new file mode 100644 index 000000000..6bd01e10e --- /dev/null +++ b/pkg/backup/mocks/item_action.go @@ -0,0 +1,65 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. +package mocks + +import backup "github.com/heptio/ark/pkg/backup" +import mock "github.com/stretchr/testify/mock" +import runtime "k8s.io/apimachinery/pkg/runtime" +import v1 "github.com/heptio/ark/pkg/apis/ark/v1" + +// ItemAction is an autogenerated mock type for the ItemAction type +type ItemAction struct { + mock.Mock +} + +// AppliesTo provides a mock function with given fields: +func (_m *ItemAction) AppliesTo() (backup.ResourceSelector, error) { + ret := _m.Called() + + var r0 backup.ResourceSelector + if rf, ok := ret.Get(0).(func() backup.ResourceSelector); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(backup.ResourceSelector) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Execute provides a mock function with given fields: item, _a1 +func (_m *ItemAction) Execute(item runtime.Unstructured, _a1 *v1.Backup) (runtime.Unstructured, []backup.ResourceIdentifier, error) { + ret := _m.Called(item, _a1) + + var r0 runtime.Unstructured + if rf, ok := ret.Get(0).(func(runtime.Unstructured, *v1.Backup) runtime.Unstructured); ok { + r0 = rf(item, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(runtime.Unstructured) + } + } + + var r1 []backup.ResourceIdentifier + if rf, ok := ret.Get(1).(func(runtime.Unstructured, *v1.Backup) []backup.ResourceIdentifier); ok { + r1 = rf(item, _a1) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]backup.ResourceIdentifier) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(runtime.Unstructured, *v1.Backup) error); ok { + r2 = rf(item, _a1) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} diff --git a/pkg/plugin/backup_item_action.go b/pkg/plugin/backup_item_action.go index 076045dc3..0e4cd54d6 100644 --- a/pkg/plugin/backup_item_action.go +++ b/pkg/plugin/backup_item_action.go @@ -171,24 +171,34 @@ func (s *BackupItemActionGRPCServer) Execute(ctx context.Context, req *proto.Exe return nil, err } - updatedItemJSON, err := json.Marshal(updatedItem.UnstructuredContent()) - if err != nil { - return nil, err + // If the plugin implementation returned a nil updatedItem (meaning no modifications), reset updatedItem to the + // original item. + var updatedItemJSON []byte + if updatedItem == nil { + updatedItemJSON = req.Item + } else { + updatedItemJSON, err = json.Marshal(updatedItem.UnstructuredContent()) + if err != nil { + return nil, err + } } res := &proto.ExecuteResponse{ Item: updatedItemJSON, } - for _, itm := range additionalItems { - val := proto.ResourceIdentifier{ - Group: itm.Group, - Resource: itm.Resource, - Namespace: itm.Namespace, - Name: itm.Name, - } - res.AdditionalItems = append(res.AdditionalItems, &val) + for _, item := range additionalItems { + res.AdditionalItems = append(res.AdditionalItems, backupResourceIdentifierToProto(item)) } return res, nil } + +func backupResourceIdentifierToProto(id arkbackup.ResourceIdentifier) *proto.ResourceIdentifier { + return &proto.ResourceIdentifier{ + Group: id.Group, + Resource: id.Resource, + Namespace: id.Namespace, + Name: id.Name, + } +} diff --git a/pkg/plugin/backup_item_action_test.go b/pkg/plugin/backup_item_action_test.go new file mode 100644 index 000000000..00179b3ae --- /dev/null +++ b/pkg/plugin/backup_item_action_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2018 the Heptio Ark 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 plugin + +import ( + "encoding/json" + "testing" + + "github.com/heptio/ark/pkg/apis/ark/v1" + "github.com/heptio/ark/pkg/backup" + "github.com/heptio/ark/pkg/backup/mocks" + proto "github.com/heptio/ark/pkg/plugin/generated" + "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" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestBackupItemActionGRPCServerExecute(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) + + updatedItem := []byte(` + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "namespace": "myns", + "name": "myconfigmap" + }, + "data": { + "key": "changed!" + } + }`) + var updatedItemObject unstructured.Unstructured + err = json.Unmarshal(updatedItem, &updatedItemObject) + require.NoError(t, err) + + invalidBackup := []byte("this is gibberish json") + validBackup := []byte(` + { + "apiVersion": "ark.heptio.com/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 + implUpdatedItem runtime.Unstructured + implAdditionalItems []backup.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: "nil updatedItem / no additionalItems", + item: validItem, + backup: validBackup, + }, + { + name: "same updatedItem / some additionalItems", + item: validItem, + backup: validBackup, + implUpdatedItem: &validItemObject, + implAdditionalItems: []backup.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{Group: "v1", Resource: "pods"}, + Namespace: "myns", + Name: "mypod", + }, + }, + }, + { + name: "different updatedItem", + item: validItem, + backup: validBackup, + implUpdatedItem: &updatedItemObject, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + itemAction := &mocks.ItemAction{} + defer itemAction.AssertExpectations(t) + + if !test.skipMock { + itemAction.On("Execute", &validItemObject, &validBackupObject).Return(test.implUpdatedItem, test.implAdditionalItems, test.implError) + } + + s := &BackupItemActionGRPCServer{impl: itemAction} + + req := &proto.ExecuteRequest{ + Item: test.item, + Backup: test.backup, + } + + resp, err := s.Execute(context.Background(), req) + + // Verify error + assert.Equal(t, test.expectError, err != nil) + if test.expectError { + return + } + + // Verify updated item + updatedItem := test.implUpdatedItem + if updatedItem == nil { + // If the impl returned nil for its updatedItem, we should expect the plugin to return the original item + updatedItem = &validItemObject + } + + var respItem unstructured.Unstructured + err = json.Unmarshal(resp.Item, &respItem) + require.NoError(t, err) + + assert.Equal(t, updatedItem, &respItem) + + // Verify additional items + var expectedAdditionalItems []*proto.ResourceIdentifier + for _, item := range test.implAdditionalItems { + expectedAdditionalItems = append(expectedAdditionalItems, backupResourceIdentifierToProto(item)) + } + assert.Equal(t, expectedAdditionalItems, resp.AdditionalItems) + }) + } +}