diff --git a/provider/pkg/provider/client/client.go b/provider/pkg/provider/client/client.go index 9b2e7459d2..9f450ecae0 100644 --- a/provider/pkg/provider/client/client.go +++ b/provider/pkg/provider/client/client.go @@ -6,9 +6,12 @@ import ( "context" "encoding/json" "fmt" + "strings" + "github.com/aws/aws-sdk-go-v2/aws/transport/http" "github.com/aws/aws-sdk-go-v2/service/cloudcontrol" "github.com/aws/aws-sdk-go-v2/service/cloudcontrol/types" + "github.com/aws/smithy-go" "github.com/mattbaird/jsonpatch" "github.com/pkg/errors" "github.com/pulumi/pulumi-aws-native/provider/pkg/schema" @@ -23,7 +26,8 @@ type CloudControlClient interface { // Read returns the current state of the specified resource. It deserializes // the response from the service into a map of untyped values. - Read(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) + // If the resource does not exist, no error is returned but the flag exists is set to false. + Read(ctx context.Context, typeName, identifier string) (resourceState map[string]interface{}, exists bool, err error) // Update updates a resource of the specified type with the specified changeset. // It awaits the operation until completion and returns a map of output property values. @@ -101,8 +105,25 @@ func (c *clientImpl) Create(ctx context.Context, typeName string, desiredState m return &id, outputs, nil } -func (c *clientImpl) Read(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) { - return c.api.GetResource(ctx, typeName, identifier) +func (c *clientImpl) Read(ctx context.Context, typeName, identifier string) (resourceState map[string]interface{}, exists bool, err error) { + resourceState, err = c.api.GetResource(ctx, typeName, identifier) + if err != nil { + var oe *smithy.OperationError + if errors.As(err, &oe) { + if re, ok := oe.Unwrap().(*http.ResponseError); ok { + statusCode := re.HTTPStatusCode() + errorMessage := re.Error() + isHttpNotFound := statusCode == 404 + isResourceNotFound := statusCode == 400 && strings.Contains(errorMessage, "ResourceNotFoundException") + if isHttpNotFound || isResourceNotFound { + return nil, false, nil + } + } + } + return nil, false, err + } + + return resourceState, true, nil } func (c *clientImpl) Update(ctx context.Context, typeName, identifier string, patches []jsonpatch.JsonPatchOperation) (map[string]interface{}, error) { diff --git a/provider/pkg/provider/client/client_test.go b/provider/pkg/provider/client/client_test.go new file mode 100644 index 0000000000..f105500d9f --- /dev/null +++ b/provider/pkg/provider/client/client_test.go @@ -0,0 +1,133 @@ +// Copyright 2016-2024, Pulumi Corporation. + +package client + +import ( + "context" + "errors" + "net/http" + "testing" + + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/service/cloudcontrol/types" + "github.com/aws/smithy-go" + smithyhttp "github.com/aws/smithy-go/transport/http" + "github.com/mattbaird/jsonpatch" + "github.com/stretchr/testify/assert" +) + +func TestClientRead(t *testing.T) { + ctx := context.TODO() + typeName := "exampleType" + identifier := "exampleIdentifier" + + // Mock API implementation + mockAPI := &mockAPI{} + client := &clientImpl{ + api: mockAPI, + } + + t.Run("Resource found", func(t *testing.T) { + resourceState := map[string]interface{}{"key": "value"} + mockAPI.GetResourceFunc = func(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) { + return resourceState, nil + } + + state, exists, err := client.Read(ctx, typeName, identifier) + + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, resourceState, state) + }) + + t.Run("Resource not found 404", func(t *testing.T) { + mockAPI.GetResourceFunc = func(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) { + return nil, &smithy.OperationError{ + Err: &awshttp.ResponseError{ + ResponseError: &smithyhttp.ResponseError{ + Response: &smithyhttp.Response{ + Response: &http.Response{ + StatusCode: 404, + }, + }, + }, + }, + } + } + + state, exists, err := client.Read(ctx, typeName, identifier) + + assert.NoError(t, err) + assert.False(t, exists) + assert.Nil(t, state) + }) + + t.Run("Resource not found ResourceNotFoundException", func(t *testing.T) { + mockAPI.GetResourceFunc = func(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) { + return nil, &smithy.OperationError{ + Err: &awshttp.ResponseError{ + ResponseError: &smithyhttp.ResponseError{ + Response: &smithyhttp.Response{ + Response: &http.Response{ + StatusCode: 400, + }, + }, + Err: errors.New("Oh no ResourceNotFoundException happened!"), + }, + }, + } + } + + state, exists, err := client.Read(ctx, typeName, identifier) + + assert.NoError(t, err) + assert.False(t, exists) + assert.Nil(t, state) + }) + + t.Run("Other error", func(t *testing.T) { + expectedErr := errors.New("some error") + mockAPI.GetResourceFunc = func(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) { + return nil, expectedErr + } + + state, exists, err := client.Read(ctx, typeName, identifier) + + assert.Equal(t, expectedErr, err) + assert.False(t, exists) + assert.Nil(t, state) + }) +} + +// Mock API implementation +type mockAPI struct { + GetResourceFunc func(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) +} + +func (m *mockAPI) GetResource(ctx context.Context, typeName, identifier string) (map[string]interface{}, error) { + return m.GetResourceFunc(ctx, typeName, identifier) +} + +func (m *mockAPI) CreateResource(ctx context.Context, cfType, desiredState string) (*types.ProgressEvent, error) { + panic("not implemented") +} + +// UpdateResource updates a resource of the specified type with the specified changeset. +// It returns a ProgressEvent which is the initial progress returned directly from the API call, +// without awaiting any long-running operations. +// The changes to be applied are expressed as a list of JSON patch operations. +func (m *mockAPI) UpdateResource(ctx context.Context, cfType, id string, patches []jsonpatch.JsonPatchOperation) (*types.ProgressEvent, error) { + panic("not implemented") +} + +// DeleteResource deletes a resource of the specified type with the given identifier. +// It returns a ProgressEvent which is the initial progress returned directly from the API call, +// without awaiting any long-running operations. +func (m *mockAPI) DeleteResource(ctx context.Context, cfType, id string) (*types.ProgressEvent, error) { + panic("not implemented") +} + +// GetResourceRequestStatus returns the current status of a resource operation request. +func (m *mockAPI) GetResourceRequestStatus(ctx context.Context, requestToken string) (*types.ProgressEvent, error) { + panic("not implemented") +} diff --git a/provider/pkg/provider/provider.go b/provider/pkg/provider/provider.go index d7b3dc89a9..c65540265b 100644 --- a/provider/pkg/provider/provider.go +++ b/provider/pkg/provider/provider.go @@ -33,7 +33,6 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/ratelimit" "github.com/aws/aws-sdk-go-v2/aws/retry" - awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" @@ -43,7 +42,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/sts" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" - "github.com/aws/smithy-go" "github.com/aws/smithy-go/middleware" smithyhttp "github.com/aws/smithy-go/transport/http" "github.com/golang/glog" @@ -619,10 +617,13 @@ func (p *cfnProvider) getInvokeFunc(ctx context.Context, tok string) (invokeFunc } identifier := strings.Join(idParts, "|") glog.V(9).Infof("%s invoking", cf.CfType) - outputs, err := p.ccc.Read(ctx, cf.CfType, identifier) + outputs, exists, err := p.ccc.Read(ctx, cf.CfType, identifier) if err != nil { return nil, err } + if !exists { + return nil, errors.New("resource not found") + } sdkOutput := schema.CfnToSdk(outputs) return resource.NewPropertyMapFromMap(sdkOutput), nil @@ -920,23 +921,14 @@ func (p *cfnProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pu if !ok { return nil, errors.Errorf("Resource type %s not found", resourceToken) } - resourceState, err := p.ccc.Read(p.canceler.context, spec.CfType, id) + resourceState, exists, err := p.ccc.Read(p.canceler.context, spec.CfType, id) if err != nil { - var oe *smithy.OperationError - if errors.As(err, &oe) { - if re, ok := oe.Unwrap().(*awshttp.ResponseError); ok { - statusCode := re.HTTPStatusCode() - errorMessage := re.Error() - isHttpNotFound := statusCode == 404 - isResourceNotFound := statusCode == 400 && strings.Contains(errorMessage, "ResourceNotFoundException") - if isHttpNotFound || isResourceNotFound { - // ResourceNotFound means that the resource was deleted. - return &pulumirpc.ReadResponse{Id: ""}, nil - } - } - } return nil, err } + if !exists { + // Not Exists means that the resource was deleted. + return &pulumirpc.ReadResponse{Id: ""}, nil + } newState := schema.CfnToSdk(resourceState) // Extract old inputs from the `__inputs` field of the old state.