diff --git a/cfn/cfn.go b/cfn/cfn.go index 251504a3..37c21486 100644 --- a/cfn/cfn.go +++ b/cfn/cfn.go @@ -2,25 +2,21 @@ package cfn import ( "context" - "encoding/json" "errors" + "io/ioutil" "log" "os" + "path" "time" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/callback" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/credentials" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/metrics" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/scheduler" "github.com/aws/aws-lambda-go/lambda" - "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudwatch" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" ) @@ -43,12 +39,6 @@ const ( listAction = "LIST" ) -// MaxRetries is the number of times to try to call the Handler after it fails to respond. -var MaxRetries int = 3 - -// Timeout is the length of time to wait before giving up on a request. -var Timeout time.Duration = 60 * time.Second - // Handler is the interface that all resource providers must implement // // Each method of Handler maps directly to a CloudFormation action. @@ -63,11 +53,6 @@ type Handler interface { List(request handler.Request) handler.ProgressEvent } -// InvokeScheduler is the interface that all reinvocation schedulers must implement -type InvokeScheduler interface { - Reschedule(lambdaCtx context.Context, secsFromNow int64, callbackRequest string, invocationIDS *scheduler.ScheduleIDS) (*scheduler.Result, error) -} - // Start is the entry point called from a resource's main function // // We define two lambda entry points; MakeEventFunc is the entry point to all @@ -109,242 +94,80 @@ type testEventFunc func(ctx context.Context, event *testEvent) (handler.Progress // handlerFunc is the signature required for all actions type handlerFunc func(request handler.Request) handler.ProgressEvent -// router decides which handler should be invoked based on the action -// It will return a route or an error depending on the action passed in -func router(a string, h Handler) (handlerFunc, error) { - // Figure out which action was called and have a "catch-all" - switch a { - case createAction: - return h.Create, nil - case readAction: - return h.Read, nil - case updateAction: - return h.Update, nil - case deleteAction: - return h.Delete, nil - case listAction: - return h.List, nil - default: - // No action matched, we should fail and return an InvalidRequestErrorCode - return nil, cfnerr.New(invalidRequestError, "No action/invalid action specified", nil) - } -} - -// Invoke handles the invocation of the handerFn. -func invoke(handlerFn handlerFunc, request handler.Request, metricsPublisher *metrics.Publisher, action string) (handler.ProgressEvent, error) { - attempts := 0 - - for { - attempts++ - // Create a context that is both manually cancellable and will signal - // a cancel at the specified duration. - ctx, cancel := context.WithTimeout(context.Background(), Timeout) - //We always defer a cancel. - defer cancel() - - // Create a channel to received a signal that work is done. - ch := make(chan handler.ProgressEvent, 1) - - // Ask the goroutine to do some work for us. - go func() { - //start the timer - start := time.Now() - metricsPublisher.PublishInvocationMetric(time.Now(), string(action)) - - // Report the work is done. - progEvt := handlerFn(request) - - marshaled, _ := encoding.Marshal(progEvt.ResourceModel) - log.Printf("Received event: %s\nMessage: %s\nBody: %s", - progEvt.OperationStatus, - progEvt.Message, - marshaled, - ) - - elapsed := time.Since(start) - metricsPublisher.PublishDurationMetric(time.Now(), string(action), elapsed.Seconds()*1e3) - ch <- progEvt - }() - - // Wait for the work to finish. If it takes too long move on. If the function returns an error, signal the error channel. - select { - case d := <-ch: - //Return the response from the handler. - return d, nil - - case <-ctx.Done(): - if attempts == MaxRetries { - log.Printf("Handler failed to respond, retrying... attempt: %v action: %s \n", attempts, action) - //handler failed to respond. - cfnErr := cfnerr.New(timeoutError, "Handler failed to respond in time", nil) - metricsPublisher.PublishExceptionMetric(time.Now(), string(action), cfnErr) - return handler.ProgressEvent{}, cfnErr - } - log.Printf("Handler failed to respond, retrying... attempt: %v action: %s \n", attempts, action) - - } - } -} - -func isMutatingAction(action string) bool { - switch action { - case createAction: - return true - case updateAction: - return true - case deleteAction: - return true - } - return false -} - -func translateStatus(operationStatus handler.Status) callback.Status { - switch operationStatus { - case handler.Success: - return callback.Success - case handler.Failed: - return callback.Failed - case handler.InProgress: - return callback.InProgress - default: - return callback.UnknownStatus - } - -} - -func processinvoke(handlerFn handlerFunc, event *event, request handler.Request, metricsPublisher *metrics.Publisher) handler.ProgressEvent { - progEvt, err := invoke(handlerFn, request, metricsPublisher, event.Action) - if err != nil { - log.Printf("Handler invocation failed: %v", err) - return handler.NewFailedEvent(err) - } - return progEvt -} - -func reschedule(ctx context.Context, invokeScheduler InvokeScheduler, progEvt handler.ProgressEvent, event *event) (bool, error) { - cusCtx, delay := marshalCallback(&progEvt) - ids, err := scheduler.GenerateCloudWatchIDS() - if err != nil { - return false, err - } - // Add IDs to recall the function with Cloudwatch events - event.RequestContext.CloudWatchEventsRuleName = ids.Handler - event.RequestContext.CloudWatchEventsTargetID = ids.Target - // Update model properties - m, err := encoding.Marshal(progEvt.ResourceModel) - if err != nil { - return false, err - } - event.RequestData.ResourceProperties = m - // Rebuild the context - event.RequestContext.CallbackContext = cusCtx - callbackRequest, err := json.Marshal(event) - if err != nil { - return false, err - } - scheResult, err := invokeScheduler.Reschedule(ctx, delay, string(callbackRequest), ids) - if err != nil { - return false, err - } - return scheResult.ComputeLocal, nil -} - // MakeEventFunc is the entry point to all invocations of a custom resource func makeEventFunc(h Handler) eventFunc { return func(ctx context.Context, event *event) (response, error) { - platformSession := credentials.SessionFromCredentialsProvider(&event.RequestData.PlatformCredentials) - providerSession := credentials.SessionFromCredentialsProvider(&event.RequestData.ProviderCredentials) - logsProvider, err := logging.NewCloudWatchLogsProvider( - cloudwatchlogs.New(providerSession), + //pls := credentials.SessionFromCredentialsProvider(&event.RequestData.PlatformCredentials) + ps := credentials.SessionFromCredentialsProvider(&event.RequestData.ProviderCredentials) + l, err := logging.NewCloudWatchLogsProvider( + cloudwatchlogs.New(ps), event.RequestData.ProviderLogGroupName, ) - // Set default logger to output to CWL in the provider account - logging.SetProviderLogOutput(logsProvider) - - metricsPublisher := metrics.New(cloudwatch.New(platformSession), event.AWSAccountID, event.ResourceType) - callbackAdapter := callback.New(cloudformation.New(platformSession), event.BearerToken) - invokeScheduler := scheduler.New(cloudwatchevents.New(platformSession)) - re := newReportErr(callbackAdapter, metricsPublisher) - + logging.SetProviderLogOutput(l) + m := metrics.New(cloudwatch.New(ps), event.AWSAccountID, event.ResourceType) + re := newReportErr(m) + if err := scrubFiles("/tmp"); err != nil { + log.Printf("Error: %v", err) + m.PublishExceptionMetric(time.Now(), event.Action, err) + } handlerFn, err := router(event.Action, h) log.Printf("Handler received the %s action", event.Action) - if err != nil { return re.report(event, "router error", err, serviceInternalError) } - if err := validateEvent(event); err != nil { return re.report(event, "validation error", err, invalidRequestError) } - - // If this invocation was triggered by a 're-invoke' CloudWatch Event, clean it up. - if event.RequestContext.CallbackContext != nil { - err := invokeScheduler.CleanupEvents(event.RequestContext.CloudWatchEventsRuleName, event.RequestContext.CloudWatchEventsTargetID) - - if err != nil { - // We will log the error in the metric, but carry on. - cfnErr := cfnerr.New(serviceInternalError, "Cloudwatch Event clean up error", err) - metricsPublisher.PublishExceptionMetric(time.Now(), string(event.Action), cfnErr) - } + request := handler.NewRequest( + event.RequestData.LogicalResourceID, + event.CallbackContext, + credentials.SessionFromCredentialsProvider(&event.RequestData.CallerCredentials), + event.RequestData.PreviousResourceProperties, + event.RequestData.ResourceProperties, + ) + p := invoke(handlerFn, request, m, event.Action) + r, err := newResponse(&p, event.BearerToken) + if err != nil { + log.Printf("Error creating response: %v", err) + return re.report(event, "Response error", err, unmarshalingError) } - - if len(event.RequestContext.CallbackContext) == 0 || event.RequestContext.Invocation == 0 { - // Acknowledge the task for first time invocation. - if err := callbackAdapter.ReportInitialStatus(); err != nil { - return re.report(event, "callback initial report error", err, serviceInternalError) - } + if !isMutatingAction(event.Action) && r.OperationStatus == handler.InProgress { + return re.report(event, "Response error", errors.New("READ and LIST handlers must return synchronous"), invalidRequestError) } + return r, nil + } +} - re.setPublishSatus(true) - for { - request := handler.NewRequest( - event.RequestData.LogicalResourceID, - event.RequestContext.CallbackContext, - credentials.SessionFromCredentialsProvider(&event.RequestData.CallerCredentials), - event.RequestData.PreviousResourceProperties, - event.RequestData.ResourceProperties, - ) - event.RequestContext.Invocation = event.RequestContext.Invocation + 1 - - progEvt := processinvoke(handlerFn, event, request, metricsPublisher) - - r, err := newResponse(&progEvt, event.BearerToken) - if err != nil { - log.Printf("Error creating response: %v", err) - return re.report(event, "Response error", err, unmarshalingError) - } - - if !isMutatingAction(event.Action) && r.OperationStatus == handler.InProgress { - return re.report(event, "Response error", errors.New("READ and LIST handlers must return synchronous"), invalidRequestError) - } - - if isMutatingAction(event.Action) { - m, err := encoding.Marshal(progEvt.ResourceModel) - if err != nil { - log.Printf("Error reporting status: %v", err) - return re.report(event, "Error", err, unmarshalingError) - } - callbackAdapter.ReportStatus(translateStatus(progEvt.OperationStatus), m, progEvt.Message, string(r.ErrorCode)) - } - - switch r.OperationStatus { - case handler.InProgress: - local, err := reschedule(ctx, invokeScheduler, progEvt, event) - - if err != nil { - return re.report(event, "Reschedule error", err, serviceInternalError) - } - - // If not computing local, exit and return response. - if !local { - return r, nil - } - default: - return r, nil - } +func scrubFiles(dir string) error { + names, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + for _, entery := range names { + os.RemoveAll(path.Join([]string{dir, entery.Name()}...)) + } + return nil +} - } +// router decides which handler should be invoked based on the action +// It will return a route or an error depending on the action passed in +func router(a string, h Handler) (handlerFunc, error) { + // Figure out which action was called and have a "catch-all" + switch a { + case createAction: + return h.Create, nil + case readAction: + return h.Read, nil + case updateAction: + return h.Update, nil + case deleteAction: + return h.Delete, nil + case listAction: + return h.List, nil + default: + // No action matched, we should fail and return an InvalidRequestErrorCode + return nil, cfnerr.New(invalidRequestError, "No action/invalid action specified", nil) } } @@ -367,3 +190,40 @@ func makeTestEventFunc(h Handler) testEventFunc { return progEvt, nil } } + +// Invoke handles the invocation of the handerFn. +func invoke(handlerFn handlerFunc, request handler.Request, metricsPublisher *metrics.Publisher, action string) handler.ProgressEvent { + + // Create a channel to received a signal that work is done. + ch := make(chan handler.ProgressEvent, 1) + + // Ask the goroutine to do some work for us. + go func() { + //start the timer + s := time.Now() + metricsPublisher.PublishInvocationMetric(time.Now(), string(action)) + + // Report the work is done. + pe := handlerFn(request) + log.Printf("Received event: %s\nMessage: %s\n", + pe.OperationStatus, + pe.Message, + ) + e := time.Since(s) + metricsPublisher.PublishDurationMetric(time.Now(), string(action), e.Seconds()*1e3) + ch <- pe + }() + return <-ch +} + +func isMutatingAction(action string) bool { + switch action { + case createAction: + return true + case updateAction: + return true + case deleteAction: + return true + } + return false +} diff --git a/cfn/cfn_test.go b/cfn/cfn_test.go index c566658d..51550d3d 100644 --- a/cfn/cfn_test.go +++ b/cfn/cfn_test.go @@ -12,64 +12,12 @@ import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/scheduler" "github.com/aws/aws-lambda-go/lambdacontext" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudformation" ) -func TestReschedule(t *testing.T) { - c := context.Background() - - p := handler.NewProgressEvent() - p.CallbackContext = map[string]interface{}{"foo": true} - e := &event{} - - s := scheduler.ScheduleIDS{ - Target: "foo", - Handler: "bar", - } - - type args struct { - ctx context.Context - invokeScheduler InvokeScheduler - progEvt handler.ProgressEvent - event *event - } - tests := []struct { - name string - args args - want bool - wantErr bool - }{ - {"Test reschedule should return true", args{c, MockScheduler{Err: nil, Result: &scheduler.Result{ComputeLocal: true, IDS: s}}, p, e}, true, false}, - {"Test reschedule should return false", args{c, MockScheduler{Err: nil, Result: &scheduler.Result{ComputeLocal: false, IDS: s}}, p, e}, false, false}, - {"Test reschedule should return error", args{c, MockScheduler{Err: errors.New("error"), Result: &scheduler.Result{ComputeLocal: true, IDS: s}}, p, e}, false, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := reschedule(tt.args.ctx, tt.args.invokeScheduler, tt.args.progEvt, tt.args.event) - if (err != nil) != tt.wantErr { - t.Errorf("reschedule() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("reschedule() = %v, want %v", got, tt.want) - } - if len(e.RequestContext.CloudWatchEventsRuleName) == 0 { - t.Errorf("RequestContext.CloudWatchEventsRuleName not set") - } - if len(e.RequestContext.CloudWatchEventsTargetID) == 0 { - t.Errorf("RequestContext.CloudWatchEventsTargetID not set") - } - if len(e.RequestContext.CallbackContext) == 0 { - t.Errorf("RequestContext.CallbackContext not set") - } - }) - } -} - func TestMakeEventFunc(t *testing.T) { start := time.Now() future := start.Add(time.Minute * 15) @@ -92,47 +40,13 @@ func TestMakeEventFunc(t *testing.T) { } } - f4 := func(callback map[string]interface{}, s *session.Session) handler.ProgressEvent { - return handler.ProgressEvent{ - OperationStatus: handler.Failed, - } - } - f3 := func(callback map[string]interface{}, s *session.Session) handler.ProgressEvent { - - if len(callback) == 1 { - return handler.ProgressEvent{ - OperationStatus: handler.Success, - Message: "Success", - } - - } - return handler.ProgressEvent{ - OperationStatus: handler.InProgress, - Message: "In Progress", - CallbackDelaySeconds: 3, - CallbackContext: map[string]interface{}{"foo": "bar"}, - } - } - - f5 := func(callback map[string]interface{}, s *session.Session) handler.ProgressEvent { - - if len(callback) == 1 { - return handler.ProgressEvent{ - OperationStatus: handler.Failed, - Message: "Failed", - } - - } return handler.ProgressEvent{ - OperationStatus: handler.InProgress, - Message: "In Progress", - CallbackDelaySeconds: 3, - CallbackContext: map[string]interface{}{"foo": "bar"}, + OperationStatus: handler.Failed, } } - f6 := func(callback map[string]interface{}, s *session.Session) (response handler.ProgressEvent) { + f4 := func(callback map[string]interface{}, s *session.Session) (response handler.ProgressEvent) { defer func() { // Catch any panics and return a failed ProgressEvent if r := recover(); r != nil { @@ -161,24 +75,15 @@ func TestMakeEventFunc(t *testing.T) { {"Test simple CREATE", args{&MockHandler{f1}, lc, loadEvent("request.create.json", &event{})}, response{ BearerToken: "123456", }, false}, - {"Test CREATE failed", args{&MockHandler{f4}, lc, loadEvent("request.create.json", &event{})}, response{ + {"Test CREATE failed", args{&MockHandler{f3}, lc, loadEvent("request.create.json", &event{})}, response{ OperationStatus: handler.Failed, BearerToken: "123456", }, false}, {"Test simple CREATE async", args{&MockHandler{f2}, lc, loadEvent("request.create.json", &event{})}, response{ - BearerToken: "123456", - Message: "In Progress", - OperationStatus: handler.InProgress, - }, false}, - {"Test CREATE async local", args{&MockHandler{f3}, lc, loadEvent("request.create.json", &event{})}, response{ - BearerToken: "123456", - Message: "Success", - OperationStatus: handler.Success, - }, false}, - {"Test CREATE async local failed", args{&MockHandler{f5}, lc, loadEvent("request.create.json", &event{})}, response{ - BearerToken: "123456", - Message: "Failed", - OperationStatus: handler.Failed, + BearerToken: "123456", + Message: "In Progress", + OperationStatus: handler.InProgress, + CallbackDelaySeconds: 130, }, false}, {"Test READ async should return err", args{&MockHandler{f2}, lc, loadEvent("request.read.json", &event{})}, response{ OperationStatus: handler.Failed, @@ -189,7 +94,7 @@ func TestMakeEventFunc(t *testing.T) { {"Test invalid Action", args{&MockHandler{f1}, context.Background(), loadEvent("request.invalid.json", &event{})}, response{ OperationStatus: handler.Failed, }, true}, - {"Test wrap panic", args{&MockHandler{f6}, context.Background(), loadEvent("request.create.json", &event{})}, response{ + {"Test wrap panic", args{&MockHandler{f4}, context.Background(), loadEvent("request.create.json", &event{})}, response{ OperationStatus: handler.Failed, ErrorCode: cloudformation.HandlerErrorCodeGeneralServiceException, Message: "Unable to complete request: error", diff --git a/cfn/entry_test.go b/cfn/entry_test.go index ad52494d..2cbcc270 100644 --- a/cfn/entry_test.go +++ b/cfn/entry_test.go @@ -5,13 +5,9 @@ import ( "io/ioutil" "os" "path/filepath" - "reflect" "testing" - "time" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/metrics" ) func TestMarshalling(t *testing.T) { @@ -126,48 +122,6 @@ func TestHandler(t *testing.T) { // no-op } -func TestInvoke(t *testing.T) { - mockClient := NewMockedMetrics() - mockPub := metrics.New(mockClient, "12345678", "foo::bar::test") - - // For test purposes, set the timeout low - Timeout = time.Second - - type args struct { - handlerFn handlerFunc - request handler.Request - reqContext *requestContext - metricsPublisher *metrics.Publisher - action string - } - tests := []struct { - name string - args args - want handler.ProgressEvent - wantErr bool - wantCount int - }{ - {"TestMaxTriesShouldReturnError ", args{func(request handler.Request) handler.ProgressEvent { - time.Sleep(2 * time.Hour) - return handler.ProgressEvent{} - }, handler.NewRequest("foo", nil, nil, nil, nil), &requestContext{}, mockPub, createAction, - }, handler.ProgressEvent{}, true, 3, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := invoke(tt.args.handlerFn, tt.args.request, tt.args.metricsPublisher, tt.args.action) - if (err != nil) != tt.wantErr { - t.Errorf("Invoke() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - - } - }) - } -} - // helper func to load fixtures from the disk func openFixture(name string) ([]byte, error) { d, err := os.Getwd() diff --git a/cfn/event.go b/cfn/event.go index 39c01247..8bcf614f 100644 --- a/cfn/event.go +++ b/cfn/event.go @@ -6,23 +6,21 @@ import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/credentials" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" - "github.com/aws/aws-sdk-go/aws/session" "gopkg.in/validator.v2" ) // Event base structure, it will be internal to the RPDK. type event struct { - AWSAccountID string `json:"awsAccountId"` - BearerToken string `json:"bearerToken" validate:"nonzero"` - Region string `json:"region" validate:"nonzero"` - Action string `json:"action"` - ResponseEndpoint string `json:"responseEndpoint" validate:"nonzero"` - ResourceType string `json:"resourceType" validate:"nonzero"` - ResourceTypeVersion encoding.Float `json:"resourceTypeVersion"` - RequestContext requestContext `json:"requestContext"` - RequestData requestData `json:"requestData"` - StackID string `json:"stackId"` + AWSAccountID string `json:"awsAccountId"` + BearerToken string `json:"bearerToken" validate:"nonzero"` + Region string `json:"region" validate:"nonzero"` + Action string `json:"action"` + ResourceType string `json:"resourceType" validate:"nonzero"` + ResourceTypeVersion encoding.Float `json:"resourceTypeVersion"` + CallbackContext map[string]interface{} `json:"callbackContext,omitempty"` + RequestData requestData `json:"requestData"` + StackID string `json:"stackId"` NextToken string } @@ -31,29 +29,15 @@ type event struct { // internal use only. type requestData struct { CallerCredentials credentials.CloudFormationCredentialsProvider `json:"callerCredentials"` - PlatformCredentials credentials.CloudFormationCredentialsProvider `json:"platformCredentials"` LogicalResourceID string `json:"logicalResourceId"` ResourceProperties json.RawMessage `json:"resourceProperties"` PreviousResourceProperties json.RawMessage `json:"previousResourceProperties"` - PreviousStackTags tags `json:"previousStackTags"` ProviderCredentials credentials.CloudFormationCredentialsProvider `json:"providerCredentials"` ProviderLogGroupName string `json:"providerLogGroupName"` StackTags tags `json:"stackTags"` SystemTags tags `json:"systemTags"` } -// requestContext handles elements such as retries and long running creations. -// -// Updating the requestContext key will do nothing in subsequent requests or retries, -// instead you should opt to return your context items in the action -type requestContext struct { - CallbackContext map[string]interface{} `json:"callbackContext,omitempty"` - CloudWatchEventsRuleName string `json:"cloudWatchEventsRuleName,omitempty"` - CloudWatchEventsTargetID string `json:"cloudWatchEventsTargetId,omitempty"` - Invocation encoding.Int `json:"invocation,omitempty"` - Session *session.Session `json:"session,omitempty"` -} - // validateEvent ensures the event struct generated from the Lambda SDK is correct // A number of the RPDK values are required to be a certain type/length func validateEvent(event *event) error { diff --git a/cfn/metrics/publisher.go b/cfn/metrics/publisher.go index e4a1d4c4..458207f9 100644 --- a/cfn/metrics/publisher.go +++ b/cfn/metrics/publisher.go @@ -45,7 +45,6 @@ func New(client cloudwatchiface.CloudWatchAPI, account string, resType string) * if len(os.Getenv("AWS_SAM_LOCAL")) > 0 { client = newNoopClient() } - rn := ResourceTypeName(resType) return &Publisher{ client: client, @@ -57,12 +56,12 @@ func New(client cloudwatchiface.CloudWatchAPI, account string, resType string) * // PublishExceptionMetric publishes an exception metric. func (p *Publisher) PublishExceptionMetric(date time.Time, action string, e error) { + v := strings.ReplaceAll(e.Error(), "\n", " ") dimensions := map[string]string{ DimensionKeyAcionType: string(action), - DimensionKeyExceptionType: e.Error(), + DimensionKeyExceptionType: v, DimensionKeyResourceType: p.resourceType, } - p.publishMetric(MetricNameHanderException, dimensions, cloudwatch.StandardUnitCount, 1.0, date) } @@ -72,9 +71,7 @@ func (p *Publisher) PublishInvocationMetric(date time.Time, action string) { DimensionKeyAcionType: string(action), DimensionKeyResourceType: p.resourceType, } - p.publishMetric(MetricNameHanderInvocationCount, dimensions, cloudwatch.StandardUnitCount, 1.0, date) - } // PublishDurationMetric publishes an duration metric. @@ -85,9 +82,7 @@ func (p *Publisher) PublishDurationMetric(date time.Time, action string, secs fl DimensionKeyAcionType: string(action), DimensionKeyResourceType: p.resourceType, } - p.publishMetric(MetricNameHanderDuration, dimensions, cloudwatch.StandardUnitMilliseconds, secs, date) - } func (p *Publisher) publishMetric(metricName string, data map[string]string, unit string, value float64, date time.Time) { @@ -101,7 +96,6 @@ func (p *Publisher) publishMetric(metricName string, data map[string]string, uni } d = append(d, dim) } - md := []*cloudwatch.MetricDatum{ &cloudwatch.MetricDatum{ MetricName: aws.String(metricName), @@ -110,14 +104,11 @@ func (p *Publisher) publishMetric(metricName string, data map[string]string, uni Dimensions: d, Timestamp: &date}, } - pi := cloudwatch.PutMetricDataInput{ Namespace: aws.String(p.namespace), MetricData: md, } - _, err := p.client.PutMetricData(&pi) - if err != nil { p.logger.Printf("An error occurred while publishing metrics: %s", err) diff --git a/cfn/metrics/publisher_test.go b/cfn/metrics/publisher_test.go index f9d3f373..9147dd07 100644 --- a/cfn/metrics/publisher_test.go +++ b/cfn/metrics/publisher_test.go @@ -80,7 +80,7 @@ func TestPublisher_PublishExceptionMetric(t *testing.T) { wantUnit string wantValue float64 }{ - {"testPublisherPublishExceptionMetric", fields{NewMockCloudWatchClient(), "foo::bar::test", "12345678"}, args{time.Now(), "CREATE", errors.New("failed to create resource")}, "HandlerException", false, "CREATE", "failed to create resource", "foo/bar/test", "HandlerException", cloudwatch.StandardUnitCount, 1.0}, + {"testPublisherPublishExceptionMetric", fields{NewMockCloudWatchClient(), "foo::bar::test", "12345678"}, args{time.Now(), "CREATE", errors.New("failed to create\nresource")}, "HandlerException", false, "CREATE", "failed to create resource", "foo/bar/test", "HandlerException", cloudwatch.StandardUnitCount, 1.0}, {"testPublisherPublishExceptionMetricWantError", fields{NewMockCloudWatchClientError(), "foo::bar::test", "12345678"}, args{time.Now(), "CREATE", errors.New("failed to create resource")}, "HandlerException", true, "CREATE", "failed to create resource", "foo/bar/test", "HandlerException", cloudwatch.StandardUnitCount, 1.0}, {"testPublisherPublishExceptionMetric", fields{NewMockCloudWatchClient(), "foo::bar::test", "12345678"}, args{time.Now(), "UPDATE", errors.New("failed to create resource")}, "HandlerException", false, "UPDATE", "failed to create resource", "foo/bar/test", "HandlerException", cloudwatch.StandardUnitCount, 1.0}, {"testPublisherPublishExceptionMetricWantError", fields{NewMockCloudWatchClientError(), "foo::bar::test", "12345678"}, args{time.Now(), "UPDATE", errors.New("failed to create resource")}, "HandlerException", true, "UPDATE", "failed to create resource", "foo/bar/test", "HandlerException", cloudwatch.StandardUnitCount, 1.0}, diff --git a/cfn/reportErr.go b/cfn/reportErr.go index faeb7f86..306c5157 100644 --- a/cfn/reportErr.go +++ b/cfn/reportErr.go @@ -2,45 +2,27 @@ package cfn import ( "fmt" - "log" "time" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/callback" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/metrics" - "github.com/aws/aws-sdk-go/service/cloudformation" ) -//reportErr is an unexported struct that handles reporting of errors. +// reportErr is an unexported struct that handles reporting of errors. type reportErr struct { - callbackAdapter *callback.CloudFormationCallbackAdapter metricsPublisher *metrics.Publisher - publishStatus bool } -//NewReportErr is a factory func that returns a pointer to a struct -func newReportErr(callbackAdapter *callback.CloudFormationCallbackAdapter, metricsPublisher *metrics.Publisher) *reportErr { +// NewReportErr is a factory func that returns a pointer to a struct +func newReportErr(metricsPublisher *metrics.Publisher) *reportErr { return &reportErr{ - callbackAdapter: callbackAdapter, metricsPublisher: metricsPublisher, - publishStatus: false, } } -//Report publishes errors and reports error status to Cloudformation. +// Report publishes errors and reports error status to Cloudformation. func (r *reportErr) report(event *event, message string, err error, errCode string) (response, error) { m := fmt.Sprintf("Unable to complete request; %s error", message) - - if isMutatingAction(event.Action) && r.publishStatus { - if reportErr := r.callbackAdapter.ReportFailureStatus(event.RequestData.ResourceProperties, cloudformation.HandlerErrorCodeInternalFailure, err); reportErr != nil { - log.Printf("Callback report error; Error: %s", reportErr.Error()) - } - } r.metricsPublisher.PublishExceptionMetric(time.Now(), string(event.Action), err) - return newFailedResponse(cfnerr.New(serviceInternalError, m, err), event.BearerToken), err } - -func (r *reportErr) setPublishSatus(report bool) { - r.publishStatus = report -} diff --git a/cfn/response.go b/cfn/response.go index 315bd17f..9ff28b5a 100644 --- a/cfn/response.go +++ b/cfn/response.go @@ -17,7 +17,7 @@ type response struct { //The operationStatus indicates whether the handler has reached a terminal //state or is still computing and requires more time to complete - OperationStatus handler.Status `json:"operationStatus,omitempty"` + OperationStatus handler.Status `json:"status,omitempty"` //ResourceModel it The output resource instance populated by a READ/LIST for //synchronous results and by CREATE/UPDATE/DELETE for final response @@ -36,6 +36,17 @@ type response struct { // NextToken the token used to request additional pages of resources for a LIST operation NextToken string `json:"nextToken,omitempty"` + + // CallbackContext is an arbitrary datum which the handler can return in an + // IN_PROGRESS event to allow the passing through of additional state or + // metadata between subsequent retries; for example to pass through a Resource + // identifier which can be used to continue polling for stabilization + CallbackContext map[string]interface{} `json:"callbackContext,omitempty"` + + // CallbackDelaySeconds will be scheduled with an initial delay of no less than the number + // of seconds specified in the progress event. Set this value to <= 0 to + // indicate no callback should be made. + CallbackDelaySeconds int64 `json:"callbackDelaySeconds,omitempty"` } // newFailedResponse returns a response pre-filled with the supplied error @@ -70,12 +81,14 @@ func newResponse(pevt *handler.ProgressEvent, bearerToken string) (response, err } resp := response{ - BearerToken: bearerToken, - Message: pevt.Message, - OperationStatus: pevt.OperationStatus, - ResourceModel: model, - ResourceModels: models, - NextToken: pevt.NextToken, + BearerToken: bearerToken, + Message: pevt.Message, + OperationStatus: pevt.OperationStatus, + ResourceModel: model, + ResourceModels: models, + NextToken: pevt.NextToken, + CallbackContext: pevt.CallbackContext, + CallbackDelaySeconds: pevt.CallbackDelaySeconds, } if pevt.HandlerErrorCode != "" { diff --git a/cfn/response_test.go b/cfn/response_test.go index c303cf44..f7296b2b 100644 --- a/cfn/response_test.go +++ b/cfn/response_test.go @@ -29,7 +29,7 @@ func TestMarshalJSON(t *testing.T) { BearerToken: "xyzzy", } - expected := `{"message":"foo","operationStatus":"SUCCESS","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy"}` + expected := `{"message":"foo","status":"SUCCESS","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy"}` actual, err := json.Marshal(r) if err != nil { diff --git a/cfn/types_test.go b/cfn/types_test.go index 26f57d87..a88eb4c9 100644 --- a/cfn/types_test.go +++ b/cfn/types_test.go @@ -1,10 +1,7 @@ package cfn import ( - "context" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/scheduler" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface" @@ -98,18 +95,6 @@ func (m *MockedMetrics) PutMetricData(in *cloudwatch.PutMetricDataInput) (*cloud return nil, nil } -//MockScheduler mocks the reinvocation scheduler. -// -// This implementation of the handlers is only used for testing. -type MockScheduler struct { - Err error - Result *scheduler.Result -} - -func (m MockScheduler) Reschedule(lambdaCtx context.Context, secsFromNow int64, callbackRequest string, invocationIDS *scheduler.ScheduleIDS) (*scheduler.Result, error) { - return m.Result, m.Err -} - // MockModel mocks a resource model // // This implementation of the handlers is only used for testing. diff --git a/go.mod b/go.mod index 16453996..fccbd295 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,10 @@ require ( 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 + github.com/google/go-github v17.0.0+incompatible + github.com/google/go-querystring v1.0.0 // indirect github.com/google/uuid v1.1.1 github.com/segmentio/ksuid v1.0.2 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 ) diff --git a/go.sum b/go.sum index 9bb802c0..d4b8057c 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,43 @@ +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/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/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= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/ksuid v1.0.2 h1:9yBfKyw4ECGTdALaF09Snw3sLJmYIX6AbPJrAy6MrDc= github.com/segmentio/ksuid v1.0.2/go.mod h1:BXuJDr2byAiHuQaQtSKoXh1J0YmUDurywOXgB2w+OSU= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +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/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= gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/python/rpdk/go/__init__.py b/python/rpdk/go/__init__.py index c918e14c..5684f64a 100644 --- a/python/rpdk/go/__init__.py +++ b/python/rpdk/go/__init__.py @@ -1,5 +1,5 @@ import logging -__version__ = "0.1.6" +__version__ = "2.0.0" logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/python/rpdk/go/codegen.py b/python/rpdk/go/codegen.py index 73ac1756..9ba6309a 100644 --- a/python/rpdk/go/codegen.py +++ b/python/rpdk/go/codegen.py @@ -24,7 +24,7 @@ LANGUAGE = "go" -DEFAULT_SETTINGS = {"protocolVersion": "1.0.0", "pluginVersion": __version__} +DEFAULT_SETTINGS = {"protocolVersion": "2.0.0", "pluginVersion": __version__} class GoExecutableNotFoundError(SysExitRecommendedError): diff --git a/python/rpdk/go/templates/Makefile b/python/rpdk/go/templates/Makefile index fd2cabe9..83fe7749 100644 --- a/python/rpdk/go/templates/Makefile +++ b/python/rpdk/go/templates/Makefile @@ -2,7 +2,7 @@ build: cfn generate - env GOOS=linux go build -ldflags="-s -w" -tags="logging callback scheduler" -o bin/handler cmd/main.go + env GOOS=linux go build -ldflags="-s -w" -tags="logging" -o bin/handler cmd/main.go test: cfn generate