From 91e1b658d055054cfe39c451cce22771dc995ffe Mon Sep 17 00:00:00 2001 From: Florian Stadler Date: Wed, 6 Nov 2024 19:51:08 +0100 Subject: [PATCH] Extend CustomResource interface to pass state for Update and Delete (#1801) Previously the Update and Delete methods for Custom Resources did not require access to the current state, but this is a common requirement for updating and deleting resources. This is also needed for supporting CloudFormation Custom Resources as those require access to state in order to complete updates and deletions. relates to https://github.com/pulumi/pulumi-cdk/issues/109 --- provider/pkg/provider/provider.go | 9 +++++++-- provider/pkg/provider/provider_test.go | 8 ++++---- provider/pkg/resources/custom.go | 4 ++-- provider/pkg/resources/extension_resource.go | 4 ++-- provider/pkg/resources/mock_custom_resource.go | 16 ++++++++-------- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/provider/pkg/provider/provider.go b/provider/pkg/provider/provider.go index 006bcc4472..1146176b63 100644 --- a/provider/pkg/provider/provider.go +++ b/provider/pkg/provider/provider.go @@ -1061,7 +1061,7 @@ func (p *cfnProvider) Update(ctx context.Context, req *pulumirpc.UpdateRequest) if customResource, ok := p.customResources[resourceToken]; ok { timeout := time.Duration(req.GetTimeout()) * time.Second // Custom resource - outputs, err = customResource.Update(ctx, urn, id, newInputs, oldInputs, timeout) + outputs, err = customResource.Update(ctx, urn, id, newInputs, oldInputs, oldState, timeout) if err != nil { return nil, err } @@ -1128,8 +1128,13 @@ func (p *cfnProvider) Delete(ctx context.Context, req *pulumirpc.DeleteRequest) return nil, errors.Wrapf(err, "failed to parse inputs for update") } + // Retrieve the state. + state, err := plugin.UnmarshalProperties(req.GetProperties(), plugin.MarshalOptions{ + Label: fmt.Sprintf("%s.state", label), KeepUnknowns: true, SkipNulls: true, KeepSecrets: true, + }) + timeout := time.Duration(req.GetTimeout()) * time.Second - err = customResource.Delete(ctx, urn, id, oldInputs, timeout) + err = customResource.Delete(ctx, urn, id, oldInputs, state, timeout) if err != nil { return nil, err } diff --git a/provider/pkg/provider/provider_test.go b/provider/pkg/provider/provider_test.go index fc2a979d83..0f54cd4c05 100644 --- a/provider/pkg/provider/provider_test.go +++ b/provider/pkg/provider/provider_test.go @@ -397,7 +397,7 @@ func TestUpdate(t *testing.T) { } t.Run("CustomResource", func(t *testing.T) { - mockCustomResource.EXPECT().Update(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), 5*time.Minute).Return( + mockCustomResource.EXPECT().Update(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), gomock.Any(), 5*time.Minute).Return( resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, nil, ) @@ -410,7 +410,7 @@ func TestUpdate(t *testing.T) { }) t.Run("CustomResource/Error", func(t *testing.T) { - mockCustomResource.EXPECT().Update(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), 5*time.Minute).Return( + mockCustomResource.EXPECT().Update(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), gomock.Any(), 5*time.Minute).Return( nil, assert.AnError, ) @@ -509,14 +509,14 @@ func TestDelete(t *testing.T) { } t.Run("CustomResource", func(t *testing.T) { - mockCustomResource.EXPECT().Delete(ctx, urn, "resource-id", gomock.Any(), gomock.Any()).Return(nil) + mockCustomResource.EXPECT().Delete(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) _, err := provider.Delete(ctx, req) assert.NoError(t, err) }) t.Run("CustomResource/Error", func(t *testing.T) { - mockCustomResource.EXPECT().Delete(ctx, urn, "resource-id", gomock.Any(), gomock.Any()).Return(assert.AnError) + mockCustomResource.EXPECT().Delete(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError) _, err := provider.Delete(ctx, req) assert.Error(t, err) diff --git a/provider/pkg/resources/custom.go b/provider/pkg/resources/custom.go index b25a6214e2..f730772ed8 100644 --- a/provider/pkg/resources/custom.go +++ b/provider/pkg/resources/custom.go @@ -18,7 +18,7 @@ type CustomResource interface { // Read returns the outputs and the updated inputs of the resource. Read(ctx context.Context, urn resource.URN, id string, oldInputs, oldOutputs resource.PropertyMap) (outputs resource.PropertyMap, inputs resource.PropertyMap, exists bool, err error) // Update applies the diff of the inputs to the resource and returns the updated outputs. - Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) + Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs, state resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) // Delete removes the resource from the cloud provider. - Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, timeout time.Duration) error + Delete(ctx context.Context, urn resource.URN, id string, inputs, state resource.PropertyMap, timeout time.Duration) error } diff --git a/provider/pkg/resources/extension_resource.go b/provider/pkg/resources/extension_resource.go index 5f20ab970b..bcaf7d5dd0 100644 --- a/provider/pkg/resources/extension_resource.go +++ b/provider/pkg/resources/extension_resource.go @@ -167,7 +167,7 @@ func (r *extensionResource) Read(ctx context.Context, urn resource.URN, id strin return CheckpointObject(newInputs, rawState), newInputs, true, nil } -func (r *extensionResource) Update(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, oldInputs resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) { +func (r *extensionResource) Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs, state resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) { var typedOldInputs ExtensionResourceInputs _, err := resourcex.Unmarshal(&typedOldInputs, oldInputs, resourcex.UnmarshalOptions{}) if err != nil { @@ -198,7 +198,7 @@ func (r *extensionResource) Update(ctx context.Context, urn resource.URN, id str return CheckpointObject(inputs, rawState), nil } -func (r *extensionResource) Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, timeout time.Duration) error { +func (r *extensionResource) Delete(ctx context.Context, urn resource.URN, id string, inputs, state resource.PropertyMap, timeout time.Duration) error { var typedInputs ExtensionResourceInputs _, err := resourcex.Unmarshal(&typedInputs, inputs, resourcex.UnmarshalOptions{}) if err != nil { diff --git a/provider/pkg/resources/mock_custom_resource.go b/provider/pkg/resources/mock_custom_resource.go index 756b1f85ec..e778268c8f 100644 --- a/provider/pkg/resources/mock_custom_resource.go +++ b/provider/pkg/resources/mock_custom_resource.go @@ -75,17 +75,17 @@ func (mr *MockCustomResourceMockRecorder) Create(ctx, urn, inputs, timeout any) } // Delete mocks base method. -func (m *MockCustomResource) Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, timeout time.Duration) error { +func (m *MockCustomResource) Delete(ctx context.Context, urn resource.URN, id string, inputs, state resource.PropertyMap, timeout time.Duration) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", ctx, urn, id, inputs, timeout) + ret := m.ctrl.Call(m, "Delete", ctx, urn, id, inputs, state, timeout) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. -func (mr *MockCustomResourceMockRecorder) Delete(ctx, urn, id, inputs, timeout any) *gomock.Call { +func (mr *MockCustomResourceMockRecorder) Delete(ctx, urn, id, inputs, state, timeout any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCustomResource)(nil).Delete), ctx, urn, id, inputs, timeout) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCustomResource)(nil).Delete), ctx, urn, id, inputs, state, timeout) } // Read mocks base method. @@ -106,16 +106,16 @@ func (mr *MockCustomResourceMockRecorder) Read(ctx, urn, id, oldInputs, oldOutpu } // Update mocks base method. -func (m *MockCustomResource) Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) { +func (m *MockCustomResource) Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs, state resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", ctx, urn, id, inputs, oldInputs, timeout) + ret := m.ctrl.Call(m, "Update", ctx, urn, id, inputs, oldInputs, state, timeout) ret0, _ := ret[0].(resource.PropertyMap) ret1, _ := ret[1].(error) return ret0, ret1 } // Update indicates an expected call of Update. -func (mr *MockCustomResourceMockRecorder) Update(ctx, urn, id, inputs, oldInputs, timeout any) *gomock.Call { +func (mr *MockCustomResourceMockRecorder) Update(ctx, urn, id, inputs, oldInputs, state, timeout any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCustomResource)(nil).Update), ctx, urn, id, inputs, oldInputs, timeout) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCustomResource)(nil).Update), ctx, urn, id, inputs, oldInputs, state, timeout) }