package cfn import ( "context" "errors" "log" "os" "sync" "time" "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/handler" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/metrics" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" ) const ( invalidRequestError = "InvalidRequest" serviceInternalError = "ServiceInternal" unmarshalingError = "UnmarshalingError" marshalingError = "MarshalingError" validationError = "Validation" timeoutError = "Timeout" sessionNotFoundError = "SessionNotFound" ) const ( unknownAction = "UNKNOWN" createAction = "CREATE" readAction = "READ" updateAction = "UPDATE" deleteAction = "DELETE" listAction = "LIST" ) var once sync.Once // Handler is the interface that all resource providers must implement // // Each method of Handler maps directly to a CloudFormation action. // Every action must return a progress event containing details of // any actions that were undertaken by the resource provider // or of any error that occurred during operation. type Handler interface { Create(request handler.Request) handler.ProgressEvent Read(request handler.Request) handler.ProgressEvent Update(request handler.Request) handler.ProgressEvent Delete(request handler.Request) handler.ProgressEvent List(request handler.Request) handler.ProgressEvent } // 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 // invocations of a custom resource and MakeTestEventFunc is the entry point that // allows the CLI's contract testing framework to invoke the resource's CRUDL handlers. func Start(h Handler) { defer func() { if r := recover(); r != nil { log.Printf("Handler panicked: %s", r) panic(r) // Continue the panic } }() log.Printf("Handler starting") lambda.Start(makeEventFunc(h)) log.Printf("Handler finished") } // Tags are stored as key/value paired strings type tags map[string]string // eventFunc is the function signature required to execute an event from the Lambda SDK type eventFunc func(ctx context.Context, event *event) (response, error) // handlerFunc is the signature required for all actions type handlerFunc func(request handler.Request) handler.ProgressEvent // 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) { ps := credentials.SessionFromCredentialsProvider(&event.RequestData.ProviderCredentials) m := metrics.New(cloudwatch.New(ps), event.ResourceType) once.Do(func() { l, err := logging.NewCloudWatchLogsProvider( cloudwatchlogs.New(ps), event.RequestData.ProviderLogGroupName, ) if err != nil { log.Printf("Error: %v, Logging to Stdout", err) m.PublishExceptionMetric(time.Now(), event.Action, err) l = os.Stdout } // Set default logger to output to CWL in the provider account logging.SetProviderLogOutput(l) }) re := newReportErr(m) handlerFn, cfnErr := router(event.Action, h) log.Printf("Handler received the %s action", event.Action) if cfnErr != nil { return re.report(event, "router error", cfnErr, serviceInternalError) } if err := validateEvent(event); err != nil { return re.report(event, "validation error", err, invalidRequestError) } rctx := handler.RequestContext{ StackID: event.StackID, Region: event.Region, AccountID: event.AWSAccountID, StackTags: event.RequestData.StackTags, SystemTags: event.RequestData.SystemTags, NextToken: event.NextToken, } request := handler.NewRequest( event.RequestData.LogicalResourceID, event.CallbackContext, rctx, 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) 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) } return r, 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, cfnerr.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 { // 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 }