Skip to content

Commit

Permalink
Add CloudFormation resource type configuration support
Browse files Browse the repository at this point in the history
  • Loading branch information
omkhegde committed Jun 23, 2021
1 parent d23e052 commit 0efcccb
Show file tree
Hide file tree
Showing 17 changed files with 286 additions and 190 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ pre-commit run --all-files
pre-commit run pytest-local
```

Use `./generate-examples.sh` to run install `cloudformation-cli-go-plugin` locally and run `cfn generate` in each example.

Getting started
---------------

Expand Down
1 change: 1 addition & 0 deletions cfn/cfn.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func makeEventFunc(h Handler) eventFunc {
credentials.SessionFromCredentialsProvider(&event.RequestData.CallerCredentials),
event.RequestData.PreviousResourceProperties,
event.RequestData.ResourceProperties,
event.RequestData.TypeConfiguration,
)
p := invoke(handlerFn, request, m, event.Action)
r, err := newResponse(&p, event.BearerToken)
Expand Down
1 change: 1 addition & 0 deletions cfn/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type requestData struct {
ProviderLogGroupName string `json:"providerLogGroupName"`
StackTags tags `json:"stackTags"`
SystemTags tags `json:"systemTags"`
TypeConfiguration json.RawMessage `json:"typeConfiguration"`
}

// validateEvent ensures the event struct generated from the Lambda SDK is correct
Expand Down
7 changes: 2 additions & 5 deletions cfn/handler/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,8 @@ type ProgressEvent struct {
// and by CREATE/UPDATE/DELETE for final response validation/confirmation
ResourceModel interface{} `json:"resourceModel,omitempty"`

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

// NextToken is the token used to request additional pages of resources for a LIST operation
NextToken string `json:"nextToken,omitempty"`
Expand Down
73 changes: 0 additions & 73 deletions cfn/handler/event_test.go

This file was deleted.

8 changes: 4 additions & 4 deletions cfn/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestNewRequest(t *testing.T) {
prev := Props{}
curr := Props{}

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

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

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

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

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

var invalid Props

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

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

var invalid Props

Expand Down
18 changes: 17 additions & 1 deletion cfn/handler/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Request struct {

previousResourcePropertiesBody []byte
resourcePropertiesBody []byte
typeConfiguration []byte
}

// RequestContext represents information about the current
Expand All @@ -61,14 +62,15 @@ type RequestContext struct {
}

// NewRequest returns a new Request based on the provided parameters
func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext, sess *session.Session, previousBody, body []byte) Request {
func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext, sess *session.Session, previousBody, body []byte, config []byte) Request {
return Request{
LogicalResourceID: id,
CallbackContext: ctx,
Session: sess,
previousResourcePropertiesBody: previousBody,
resourcePropertiesBody: body,
RequestContext: requestCTX,
typeConfiguration: config,
}
}

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

return nil
}

// UnmarshalType populates the provided interface
// with the current resource type configuration
func (r *Request) UnmarshalType(v interface{}) error {
if len(r.resourcePropertiesBody) == 0 {
return cfnerr.New(bodyEmptyError, "Body is empty", nil)
}

if err := encoding.Unmarshal(r.typeConfiguration, v); err != nil {
return cfnerr.New(marshalingError, "Unable to convert type", err)
}

return nil
}
7 changes: 2 additions & 5 deletions cfn/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@ type response struct {
//passed back to CloudFormation
BearerToken string `json:"bearerToken,omitempty"`

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

// NextToken the token used to request additional pages of resources for a LIST operation
NextToken string `json:"nextToken,omitempty"`
Expand Down
69 changes: 17 additions & 52 deletions cfn/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,66 +12,31 @@ import (
"github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler"
)

func TestResponseMarshalJSON(t *testing.T) {
func TestMarshalJSON(t *testing.T) {
type Model struct {
Name *encoding.String
Version *encoding.Float
}

for _, tt := range []struct {
name string
response response
expected string
}{
{
name: "updated failed",
response: response{
Message: "foo",
OperationStatus: handler.Failed,
ResourceModel: Model{
Name: encoding.NewString("Douglas"),
Version: encoding.NewFloat(42.1),
},
ErrorCode: cloudformation.HandlerErrorCodeNotUpdatable,
BearerToken: "xyzzy",
},
expected: `{"message":"foo","status":"FAILED","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy","resourceModels":null}`,
r := response{
Message: "foo",
OperationStatus: handler.Success,
ResourceModel: Model{
Name: encoding.NewString("Douglas"),
Version: encoding.NewFloat(42.1),
},
{
name: "list with 1 result",
response: response{
OperationStatus: handler.Success,
ResourceModels: []interface{}{
Model{
Name: encoding.NewString("Douglas"),
Version: encoding.NewFloat(42.1),
},
},
BearerToken: "xyzzy",
},
expected: `{"status":"SUCCESS","bearerToken":"xyzzy","resourceModels":[{"Name":"Douglas","Version":"42.1"}]}`,
},
{
name: "list with empty array",
response: response{
OperationStatus: handler.Success,
ResourceModels: []interface{}{},
BearerToken: "xyzzy",
},
expected: `{"status":"SUCCESS","bearerToken":"xyzzy","resourceModels":[]}`,
},
} {
t.Run(tt.name, func(t *testing.T) {
ErrorCode: cloudformation.HandlerErrorCodeNotUpdatable,
BearerToken: "xyzzy",
}

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

if diff := cmp.Diff(string(actual), tt.expected); diff != "" {
t.Errorf(diff)
}
})
actual, err := json.Marshal(r)
if err != nil {
t.Errorf("Unexpected error marshaling response JSON: %s", err)
}

if diff := cmp.Diff(string(actual), expected); diff != "" {
t.Errorf(diff)
}
}
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/aws-cloudformation/cloudformation-cli-go-plugin
go 1.13

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

0 comments on commit 0efcccb

Please sign in to comment.