Skip to content

Commit 6252f73

Browse files
fix: always return PhysicalResourceID for CFn CustomResources (#613)
Resolves #107
1 parent d4fbc0b commit 6252f73

2 files changed

Lines changed: 100 additions & 35 deletions

File tree

cfn/wrap.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"net/http"
1111

1212
"github.com/aws/aws-lambda-go/events"
13-
"github.com/aws/aws-lambda-go/lambdacontext"
1413
)
1514

1615
// CustomResourceLambdaFunction is a standard form Lambda for a Custom Resource.
@@ -29,11 +28,19 @@ func lambdaWrapWithClient(lambdaFunction CustomResourceFunction, client httpClie
2928
fn = func(ctx context.Context, event Event) (reason string, err error) {
3029
r := NewResponse(&event)
3130

31+
// A previous physical resource id exists unless this is a create request.
32+
fallbackPhysicalResourceID := event.PhysicalResourceID
33+
if event.RequestType == RequestCreate {
34+
// If this is a create request, the fallback should be the request ID
35+
fallbackPhysicalResourceID = event.RequestID
36+
}
37+
3238
funcDidPanic := true
3339
defer func() {
3440
if funcDidPanic {
3541
r.Status = StatusFailed
3642
r.Reason = "Function panicked, see log stream for details"
43+
r.PhysicalResourceID = fallbackPhysicalResourceID
3744
// FIXME: something should be done if an error is returned here
3845
_ = r.sendWith(client)
3946
}
@@ -42,17 +49,17 @@ func lambdaWrapWithClient(lambdaFunction CustomResourceFunction, client httpClie
4249
r.PhysicalResourceID, r.Data, err = lambdaFunction(ctx, event)
4350
funcDidPanic = false
4451

52+
if r.PhysicalResourceID == "" {
53+
r.PhysicalResourceID = fallbackPhysicalResourceID
54+
log.Printf("PhysicalResourceID not set. Using fallback PhysicalResourceID: %s\n", r.PhysicalResourceID)
55+
}
56+
4557
if err != nil {
4658
r.Status = StatusFailed
4759
r.Reason = err.Error()
48-
log.Printf("sending status failed: %s", r.Reason)
60+
log.Printf("sending status failed: %s\n", r.Reason)
4961
} else {
5062
r.Status = StatusSuccess
51-
52-
if r.PhysicalResourceID == "" {
53-
log.Println("PhysicalResourceID must exist on creation, copying Log Stream name")
54-
r.PhysicalResourceID = lambdacontext.LogStreamName
55-
}
5663
}
5764

5865
err = r.sendWith(client)

cfn/wrap_test.go

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,103 @@ import (
77
"context"
88
"encoding/json"
99
"errors"
10+
"fmt"
1011
"io/ioutil" //nolint: staticcheck
1112
"net/http"
1213
"testing"
1314

14-
"github.com/aws/aws-lambda-go/lambdacontext"
1515
"github.com/stretchr/testify/assert"
1616
)
1717

1818
var testEvent = &Event{
19-
RequestType: RequestCreate,
20-
RequestID: "unique id for this create request",
21-
ResponseURL: "http://pre-signed-S3-url-for-response",
22-
LogicalResourceID: "MyTestResource",
23-
StackID: "arn:aws:cloudformation:us-west-2:EXAMPLE/stack-name/guid",
19+
RequestType: RequestUpdate,
20+
RequestID: "unique id for this create request",
21+
ResponseURL: "http://pre-signed-S3-url-for-response",
22+
LogicalResourceID: "MyTestResource",
23+
PhysicalResourceID: "prevPhysicalResourceID",
24+
StackID: "arn:aws:cloudformation:us-west-2:EXAMPLE/stack-name/guid",
2425
}
2526

26-
func TestCopyLambdaLogStream(t *testing.T) {
27-
lgs := lambdacontext.LogStreamName
28-
lambdacontext.LogStreamName = "DUMMYLOGSTREAMNAME"
27+
func TestLambdaPhysicalResourceId(t *testing.T) {
28+
29+
tests := []struct {
30+
// Input to the lambda
31+
inputRequestType RequestType
32+
33+
// Output from the lambda
34+
returnErr error
35+
returnPhysicalResourceID string
36+
37+
// The PhysicalResourceID to test
38+
expectedPhysicalResourceID string
39+
}{
40+
// For Create with no returned PhysicalResourceID
41+
{RequestCreate, nil, "", testEvent.RequestID}, // Use RequestID as default for PhysicalResourceID
42+
{RequestCreate, fmt.Errorf("dummy error"), "", testEvent.RequestID},
43+
44+
// For Create with PhysicalResourceID
45+
{RequestCreate, nil, "newPhysicalResourceID", "newPhysicalResourceID"},
46+
{RequestCreate, fmt.Errorf("dummy error"), "newPhysicalResourceID", "newPhysicalResourceID"},
47+
48+
// For Update with no returned PhysicalResourceID
49+
{RequestUpdate, nil, "", "prevPhysicalResourceID"},
50+
{RequestUpdate, fmt.Errorf("dummy error"), "", "prevPhysicalResourceID"},
51+
52+
// For Update with returned PhysicalResourceID
53+
{RequestUpdate, nil, "newPhysicalResourceID", "newPhysicalResourceID"},
54+
{RequestUpdate, fmt.Errorf("dummy error"), "newPhysicalResourceID", "newPhysicalResourceID"},
55+
56+
// For Delete with no returned PhysicalResourceID
57+
{RequestDelete, nil, "", "prevPhysicalResourceID"},
58+
{RequestDelete, fmt.Errorf("dummy error"), "", "prevPhysicalResourceID"},
59+
60+
// For Delete with returned PhysicalResourceID = old PhysicalResourceID
61+
{RequestDelete, nil, "prevPhysicalResourceID", "prevPhysicalResourceID"},
62+
{RequestDelete, fmt.Errorf("dummy error"), "prevPhysicalResourceID", "prevPhysicalResourceID"},
63+
64+
// For Delete with returned PhysicalResourceID != old PhysicalResourceID
65+
// Technically, lambda handlers shouldn't return a different physical resource id upon deletion.
66+
// Typescript CDK implementation for handlers would return an error here.
67+
// But CFn ignores the returned PhysicalResourceID for deletion so it doesn't matter.
68+
{RequestDelete, nil, "newPhysicalResourceID", "newPhysicalResourceID"},
69+
{RequestDelete, fmt.Errorf("dummy error"), "newPhysicalResourceID", "newPhysicalResourceID"},
70+
}
71+
for _, test := range tests {
2972

30-
client := &mockClient{
31-
DoFunc: func(req *http.Request) (*http.Response, error) {
32-
response := extractResponseBody(t, req)
73+
curTestEvent := *testEvent
74+
curTestEvent.RequestType = test.inputRequestType
3375

34-
assert.Equal(t, StatusSuccess, response.Status)
35-
assert.Equal(t, testEvent.LogicalResourceID, response.LogicalResourceID)
36-
assert.Equal(t, "DUMMYLOGSTREAMNAME", response.PhysicalResourceID)
76+
if curTestEvent.RequestType == RequestCreate {
77+
curTestEvent.PhysicalResourceID = ""
78+
}
3779

38-
return &http.Response{
39-
StatusCode: http.StatusOK,
40-
Body: nopCloser{bytes.NewBufferString("")},
41-
}, nil
42-
},
43-
}
80+
client := &mockClient{
81+
DoFunc: func(req *http.Request) (*http.Response, error) {
82+
response := extractResponseBody(t, req)
4483

45-
fn := func(ctx context.Context, event Event) (physicalResourceID string, data map[string]interface{}, err error) {
46-
return
47-
}
84+
if test.returnErr == nil {
85+
assert.Equal(t, StatusSuccess, response.Status)
86+
} else {
87+
assert.Equal(t, StatusFailed, response.Status)
88+
}
4889

49-
_, err := lambdaWrapWithClient(fn, client)(context.TODO(), *testEvent)
50-
assert.NoError(t, err)
51-
lambdacontext.LogStreamName = lgs
90+
assert.Equal(t, curTestEvent.LogicalResourceID, response.LogicalResourceID)
91+
assert.Equal(t, test.expectedPhysicalResourceID, response.PhysicalResourceID)
92+
93+
return &http.Response{
94+
StatusCode: http.StatusOK,
95+
Body: nopCloser{bytes.NewBufferString("")},
96+
}, nil
97+
},
98+
}
99+
100+
fn := func(ctx context.Context, event Event) (physicalResourceID string, data map[string]interface{}, err error) {
101+
return test.returnPhysicalResourceID, nil, test.returnErr
102+
}
103+
104+
_, err := lambdaWrapWithClient(fn, client)(context.TODO(), curTestEvent)
105+
assert.NoError(t, err)
106+
}
52107
}
53108

54109
func TestPanicSendsFailure(t *testing.T) {
@@ -58,6 +113,9 @@ func TestPanicSendsFailure(t *testing.T) {
58113
DoFunc: func(req *http.Request) (*http.Response, error) {
59114
response := extractResponseBody(t, req)
60115
assert.Equal(t, StatusFailed, response.Status)
116+
117+
// Even in a panic, a dummy PhysicalResourceID should be returned to ensure the error is surfaced correctly
118+
assert.Equal(t, testEvent.PhysicalResourceID, response.PhysicalResourceID)
61119
didSendStatus = response.Status == StatusFailed
62120

63121
return &http.Response{
@@ -111,7 +169,7 @@ func TestWrappedError(t *testing.T) {
111169
response := extractResponseBody(t, req)
112170

113171
assert.Equal(t, StatusFailed, response.Status)
114-
assert.Empty(t, response.PhysicalResourceID)
172+
assert.Equal(t, testEvent.PhysicalResourceID, response.PhysicalResourceID)
115173
assert.Equal(t, "failed to create resource", response.Reason)
116174

117175
return &http.Response{

0 commit comments

Comments
 (0)