Skip to content

Commit 088839f

Browse files
committed
Add CloudFormation resource type configuration support
1 parent d23e052 commit 088839f

18 files changed

+287
-193
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ pre-commit run --all-files
4545
pre-commit run pytest-local
4646
```
4747

48-
Use `./generate-examples.sh` to run install `cloudformation-cli-go-plugin` locally and run `cfn generate` in each example.
49-
5048
Getting started
5149
---------------
5250

cfn/cfn.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func makeEventFunc(h Handler) eventFunc {
130130
credentials.SessionFromCredentialsProvider(&event.RequestData.CallerCredentials),
131131
event.RequestData.PreviousResourceProperties,
132132
event.RequestData.ResourceProperties,
133+
event.RequestData.TypeConfiguration,
133134
)
134135
p := invoke(handlerFn, request, m, event.Action)
135136
r, err := newResponse(&p, event.BearerToken)

cfn/event.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type requestData struct {
3636
ProviderLogGroupName string `json:"providerLogGroupName"`
3737
StackTags tags `json:"stackTags"`
3838
SystemTags tags `json:"systemTags"`
39+
TypeConfiguration json.RawMessage `json:"typeConfiguration"`
3940
}
4041

4142
// validateEvent ensures the event struct generated from the Lambda SDK is correct

cfn/handler/event.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,8 @@ type ProgressEvent struct {
3434
// and by CREATE/UPDATE/DELETE for final response validation/confirmation
3535
ResourceModel interface{} `json:"resourceModel,omitempty"`
3636

37-
// ResourceModels is the output resource instances populated by a LIST for
38-
// synchronous results. ResourceModels must be returned by LIST so it's
39-
// always included in the response. When ResourceModels is not set, null is
40-
// returned.
41-
ResourceModels []interface{} `json:"resourceModels"`
37+
// ResourceModels is the output resource instances populated by a LIST for synchronous results
38+
ResourceModels []interface{} `json:"resourceModels,omitempty"`
4239

4340
// NextToken is the token used to request additional pages of resources for a LIST operation
4441
NextToken string `json:"nextToken,omitempty"`

cfn/handler/event_test.go

Lines changed: 0 additions & 73 deletions
This file was deleted.

cfn/handler/handler_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestNewRequest(t *testing.T) {
1717
prev := Props{}
1818
curr := Props{}
1919

20-
req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "red"}`), []byte(`{"color": "green"}`))
20+
req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "red"}`), []byte(`{"color": "green"}`), []byte(``))
2121

2222
if err := req.UnmarshalPrevious(&prev); err != nil {
2323
t.Fatalf("Unable to unmarshal props: %v", err)
@@ -43,7 +43,7 @@ func TestNewRequest(t *testing.T) {
4343

4444
t.Run("ResourceProps", func(t *testing.T) {
4545
t.Run("Invalid Body", func(t *testing.T) {
46-
req := NewRequest("foo", nil, rctx, nil, []byte(``), []byte(``))
46+
req := NewRequest("foo", nil, rctx, nil, []byte(``), []byte(``), []byte(``))
4747

4848
invalid := struct {
4949
Color *int `json:"color"`
@@ -61,7 +61,7 @@ func TestNewRequest(t *testing.T) {
6161
})
6262

6363
t.Run("Invalid Marshal", func(t *testing.T) {
64-
req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "ref"}`), []byte(`---BAD JSON---`))
64+
req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "ref"}`), []byte(`---BAD JSON---`), []byte(``))
6565

6666
var invalid Props
6767

@@ -79,7 +79,7 @@ func TestNewRequest(t *testing.T) {
7979

8080
t.Run("PreviousResourceProps", func(t *testing.T) {
8181
t.Run("Invalid Marshal", func(t *testing.T) {
82-
req := NewRequest("foo", nil, rctx, nil, []byte(`---BAD JSON---`), []byte(`{"color": "green"}`))
82+
req := NewRequest("foo", nil, rctx, nil, []byte(`---BAD JSON---`), []byte(`{"color": "green"}`), []byte(``))
8383

8484
var invalid Props
8585

cfn/handler/request.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type Request struct {
3636

3737
previousResourcePropertiesBody []byte
3838
resourcePropertiesBody []byte
39+
typeConfiguration []byte
3940
}
4041

4142
// RequestContext represents information about the current
@@ -61,14 +62,15 @@ type RequestContext struct {
6162
}
6263

6364
// NewRequest returns a new Request based on the provided parameters
64-
func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext, sess *session.Session, previousBody, body []byte) Request {
65+
func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext, sess *session.Session, previousBody, body []byte, config []byte) Request {
6566
return Request{
6667
LogicalResourceID: id,
6768
CallbackContext: ctx,
6869
Session: sess,
6970
previousResourcePropertiesBody: previousBody,
7071
resourcePropertiesBody: body,
7172
RequestContext: requestCTX,
73+
typeConfiguration: config,
7274
}
7375
}
7476

@@ -99,3 +101,17 @@ func (r *Request) Unmarshal(v interface{}) error {
99101

100102
return nil
101103
}
104+
105+
// UnmarshalType populates the provided interface
106+
// with the current resource type configuration
107+
func (r *Request) UnmarshalType(v interface{}) error {
108+
if len(r.resourcePropertiesBody) == 0 {
109+
return cfnerr.New(bodyEmptyError, "Body is empty", nil)
110+
}
111+
112+
if err := encoding.Unmarshal(r.typeConfiguration, v); err != nil {
113+
return cfnerr.New(marshalingError, "Unable to convert type", err)
114+
}
115+
116+
return nil
117+
}

cfn/response.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,8 @@ type response struct {
3131
//passed back to CloudFormation
3232
BearerToken string `json:"bearerToken,omitempty"`
3333

34-
// ResourceModels is the output resource instances populated by a LIST for
35-
// synchronous results. ResourceModels must be returned by LIST so it's
36-
// always included in the response. When ResourceModels is not set, null is
37-
// returned.
38-
ResourceModels []interface{} `json:"resourceModels"`
34+
// ResourceModels is the output resource instances populated by a LIST for synchronous results
35+
ResourceModels []interface{} `json:"resourceModels,omitempty"`
3936

4037
// NextToken the token used to request additional pages of resources for a LIST operation
4138
NextToken string `json:"nextToken,omitempty"`

cfn/response_test.go

Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,66 +12,31 @@ import (
1212
"github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler"
1313
)
1414

15-
func TestResponseMarshalJSON(t *testing.T) {
15+
func TestMarshalJSON(t *testing.T) {
1616
type Model struct {
1717
Name *encoding.String
1818
Version *encoding.Float
1919
}
2020

21-
for _, tt := range []struct {
22-
name string
23-
response response
24-
expected string
25-
}{
26-
{
27-
name: "updated failed",
28-
response: response{
29-
Message: "foo",
30-
OperationStatus: handler.Failed,
31-
ResourceModel: Model{
32-
Name: encoding.NewString("Douglas"),
33-
Version: encoding.NewFloat(42.1),
34-
},
35-
ErrorCode: cloudformation.HandlerErrorCodeNotUpdatable,
36-
BearerToken: "xyzzy",
37-
},
38-
expected: `{"message":"foo","status":"FAILED","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy","resourceModels":null}`,
21+
r := response{
22+
Message: "foo",
23+
OperationStatus: handler.Success,
24+
ResourceModel: Model{
25+
Name: encoding.NewString("Douglas"),
26+
Version: encoding.NewFloat(42.1),
3927
},
40-
{
41-
name: "list with 1 result",
42-
response: response{
43-
OperationStatus: handler.Success,
44-
ResourceModels: []interface{}{
45-
Model{
46-
Name: encoding.NewString("Douglas"),
47-
Version: encoding.NewFloat(42.1),
48-
},
49-
},
50-
BearerToken: "xyzzy",
51-
},
52-
expected: `{"status":"SUCCESS","bearerToken":"xyzzy","resourceModels":[{"Name":"Douglas","Version":"42.1"}]}`,
53-
},
54-
{
55-
name: "list with empty array",
56-
response: response{
57-
OperationStatus: handler.Success,
58-
ResourceModels: []interface{}{},
59-
BearerToken: "xyzzy",
60-
},
61-
expected: `{"status":"SUCCESS","bearerToken":"xyzzy","resourceModels":[]}`,
62-
},
63-
} {
64-
t.Run(tt.name, func(t *testing.T) {
28+
ErrorCode: cloudformation.HandlerErrorCodeNotUpdatable,
29+
BearerToken: "xyzzy",
30+
}
6531

66-
actual, err := json.Marshal(tt.response)
67-
if err != nil {
68-
t.Errorf("Unexpected error marshaling response JSON: %s", err)
69-
}
32+
expected := `{"message":"foo","status":"SUCCESS","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy"}`
7033

71-
if diff := cmp.Diff(string(actual), tt.expected); diff != "" {
72-
t.Errorf(diff)
73-
}
74-
})
34+
actual, err := json.Marshal(r)
35+
if err != nil {
36+
t.Errorf("Unexpected error marshaling response JSON: %s", err)
7537
}
7638

39+
if diff := cmp.Diff(string(actual), expected); diff != "" {
40+
t.Errorf(diff)
41+
}
7742
}

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ module github.com/aws-cloudformation/cloudformation-cli-go-plugin
33
go 1.13
44

55
require (
6-
github.com/avast/retry-go v2.6.0+incompatible
76
github.com/aws/aws-lambda-go v1.13.3
87
github.com/aws/aws-sdk-go v1.25.37
98
github.com/google/go-cmp v0.3.1

go.sum

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
22
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3-
github.com/avast/retry-go v2.6.0+incompatible h1:FelcMrm7Bxacr1/RM8+/eqkDkmVN7tjlsy51dOzB3LI=
4-
github.com/avast/retry-go v2.6.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
53
github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY=
64
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
75
github.com/aws/aws-sdk-go v1.25.37 h1:gBtB/F3dophWpsUQKN/Kni+JzYEH2mGHF4hWNtfED1w=
86
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
97
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
108
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
119
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12-
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
1310
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
1411
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
1512
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -38,7 +35,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BG
3835
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
3936
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
4037
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
41-
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
4238
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
4339
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4440
gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 h1:2QQcyaEBdpfjjYkF0MXc69jZbHb4IOYuXz2UwsmVM8k=

0 commit comments

Comments
 (0)