Skip to content

Commit

Permalink
Move ResourceNotFound error handling to the api level
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhailshilkov committed Mar 14, 2024
1 parent 6c27c27 commit 5c9ee31
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 20 deletions.
27 changes: 24 additions & 3 deletions provider/pkg/provider/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
133 changes: 133 additions & 0 deletions provider/pkg/provider/client/client_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
26 changes: 9 additions & 17 deletions provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 5c9ee31

Please sign in to comment.