diff --git a/cfn/callback/callback.go b/cfn/callback/callback.go index a4974ff3..e5b475bc 100644 --- a/cfn/callback/callback.go +++ b/cfn/callback/callback.go @@ -1,6 +1,3 @@ -//go:build callback -// +build callback - /* Package callback provides functions for creating resource providers that may need to be called multiple times while waiting @@ -9,33 +6,36 @@ for resources to settle. package callback import ( - "fmt" + "context" "log" "github.com/avast/retry-go" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudformation" + cftypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" ) const ( - // ServiceInternalError ... ServiceInternalError string = "ServiceInternal" - // MaxRetries is the number of retries allowed to report status. - MaxRetries uint = 3 + MaxRetries uint = 3 ) +// CloudFormationClient is the subset of the CloudFormation API used by this package. +type CloudFormationClient interface { + RecordHandlerProgress(ctx context.Context, params *cloudformation.RecordHandlerProgressInput, optFns ...func(*cloudformation.Options)) (*cloudformation.RecordHandlerProgressOutput, error) +} + // CloudFormationCallbackAdapter used to report progress events back to CloudFormation. type CloudFormationCallbackAdapter struct { - client cloudformationiface.CloudFormationAPI + client CloudFormationClient bearerToken string logger *log.Logger } // New creates a CloudFormationCallbackAdapter and returns a pointer to the struct. -func New(client cloudformationiface.CloudFormationAPI, bearerToken string) *CloudFormationCallbackAdapter { +func New(client CloudFormationClient, bearerToken string) *CloudFormationCallbackAdapter { return &CloudFormationCallbackAdapter{ client: client, bearerToken: bearerToken, @@ -43,68 +43,52 @@ func New(client cloudformationiface.CloudFormationAPI, bearerToken string) *Clou } } -// ReportStatus reports the status back to the Cloudformation service of a handler -// that has moved from Pending to In_Progress +// ReportStatus reports the status back to the CloudFormation service. func (c *CloudFormationCallbackAdapter) ReportStatus(operationStatus Status, model []byte, message string, errCode string) error { - if err := c.reportProgress(errCode, operationStatus, InProgress, model, message); err != nil { - return err - } - return nil + return c.reportProgress(errCode, operationStatus, InProgress, model, message) } -// ReportInitialStatus reports the initial status back to the Cloudformation service. +// ReportInitialStatus reports the initial status back to the CloudFormation service. func (c *CloudFormationCallbackAdapter) ReportInitialStatus() error { - if err := c.reportProgress("", InProgress, Pending, []byte(""), ""); err != nil { - return err - } - return nil + return c.reportProgress("", InProgress, Pending, []byte(""), "") } -// ReportFailureStatus reports the failure status back to the Cloudformation service. +// ReportFailureStatus reports the failure status back to the CloudFormation service. func (c *CloudFormationCallbackAdapter) ReportFailureStatus(model []byte, errCode string, handlerError error) error { - if err := c.reportProgress(errCode, Failed, InProgress, model, handlerError.Error()); err != nil { - return err - } - return nil + return c.reportProgress(errCode, Failed, InProgress, model, handlerError.Error()) } -// ReportProgress reports the current status back to the Cloudformation service. func (c *CloudFormationCallbackAdapter) reportProgress(errCode string, operationStatus Status, currentOperationStatus Status, resourceModel []byte, statusMessage string) error { - - in := cloudformation.RecordHandlerProgressInput{ + in := &cloudformation.RecordHandlerProgressInput{ BearerToken: aws.String(c.bearerToken), - OperationStatus: aws.String(TranslateOperationStatus(operationStatus)), + OperationStatus: cftypes.OperationStatus(TranslateOperationStatus(operationStatus)), } if len(statusMessage) != 0 { - in.SetStatusMessage(statusMessage) + in.StatusMessage = aws.String(statusMessage) } if len(resourceModel) != 0 { - in.SetResourceModel(string(resourceModel)) + in.ResourceModel = aws.String(string(resourceModel)) } if len(errCode) != 0 { - in.SetErrorCode(TranslateErrorCode(errCode)) + in.ErrorCode = cftypes.HandlerErrorCode(TranslateErrorCode(errCode)) } if len(currentOperationStatus) != 0 { - in.SetCurrentOperationStatus(TranslateOperationStatus(currentOperationStatus)) + in.CurrentOperationStatus = cftypes.OperationStatus(TranslateOperationStatus(currentOperationStatus)) } - // Do retries and emit logs. rerr := retry.Do( func() error { - _, err := c.client.RecordHandlerProgress(&in) - if err != nil { - return err - } - return nil - }, retry.OnRetry(func(n uint, err error) { - s := fmt.Sprintf("Failed to record progress: try:#%d: %s\n ", n+1, err) - c.logger.Println(s) - - }), retry.Attempts(MaxRetries), + _, err := c.client.RecordHandlerProgress(context.Background(), in) + return err + }, + retry.OnRetry(func(n uint, err error) { + c.logger.Printf("Failed to record progress: try:#%d: %s\n", n+1, err) + }), + retry.Attempts(MaxRetries), ) if rerr != nil { @@ -114,47 +98,43 @@ func (c *CloudFormationCallbackAdapter) reportProgress(errCode string, operation return nil } -// TranslateErrorCode : Translate the error code into a standard Cloudformation error +// TranslateErrorCode translates an error code into a standard CloudFormation error. func TranslateErrorCode(errorCode string) string { - - // Ensure the error code conforms to one of the available - switch errorCode { - case cloudformation.HandlerErrorCodeNotUpdatable, - cloudformation.HandlerErrorCodeInvalidRequest, - cloudformation.HandlerErrorCodeAccessDenied, - cloudformation.HandlerErrorCodeInvalidCredentials, - cloudformation.HandlerErrorCodeAlreadyExists, - cloudformation.HandlerErrorCodeNotFound, - cloudformation.HandlerErrorCodeResourceConflict, - cloudformation.HandlerErrorCodeThrottling, - cloudformation.HandlerErrorCodeServiceLimitExceeded, - cloudformation.HandlerErrorCodeNotStabilized, - cloudformation.HandlerErrorCodeGeneralServiceException, - cloudformation.HandlerErrorCodeServiceInternalError, - cloudformation.HandlerErrorCodeNetworkFailure, - cloudformation.HandlerErrorCodeInternalFailure: + switch cftypes.HandlerErrorCode(errorCode) { + case cftypes.HandlerErrorCodeNotUpdatable, + cftypes.HandlerErrorCodeInvalidRequest, + cftypes.HandlerErrorCodeAccessDenied, + cftypes.HandlerErrorCodeInvalidCredentials, + cftypes.HandlerErrorCodeAlreadyExists, + cftypes.HandlerErrorCodeNotFound, + cftypes.HandlerErrorCodeResourceConflict, + cftypes.HandlerErrorCodeThrottling, + cftypes.HandlerErrorCodeServiceLimitExceeded, + cftypes.HandlerErrorCodeServiceTimeout, + cftypes.HandlerErrorCodeGeneralServiceException, + cftypes.HandlerErrorCodeServiceInternalError, + cftypes.HandlerErrorCodeNetworkFailure, + cftypes.HandlerErrorCodeInternalFailure: return errorCode default: - // InternalFailure is CloudFormation's fallback error code when no more specificity is there - return cloudformation.HandlerErrorCodeInternalFailure + return string(cftypes.HandlerErrorCodeInternalFailure) } } -// TranslateOperationStatus Translate the operation Status into a standard Cloudformation error +// TranslateOperationStatus translates an operation status. func TranslateOperationStatus(operationStatus Status) string { - switch operationStatus { case Success: - return cloudformation.OperationStatusSuccess + return string(cftypes.OperationStatusSuccess) case Failed: - return cloudformation.OperationStatusFailed + return string(cftypes.OperationStatusFailed) case InProgress: - return cloudformation.OperationStatusInProgress + return string(cftypes.OperationStatusInProgress) case Pending: - return cloudformation.OperationStatusPending + return string(cftypes.OperationStatusPending) default: - // default will be to fail on unknown status - return cloudformation.OperationStatusFailed + return string(cftypes.OperationStatusFailed) } - } + + diff --git a/cfn/callback/callback_notag.go b/cfn/callback/callback_notag.go deleted file mode 100644 index 36098acc..00000000 --- a/cfn/callback/callback_notag.go +++ /dev/null @@ -1,126 +0,0 @@ -//go:build !callback -// +build !callback - -package callback - -import ( - "log" - - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" -) - -// CloudFormationCallbackAdapter used to report progress events back to CloudFormation. -type CloudFormationCallbackAdapter struct { - logger *log.Logger - client cloudformationiface.CloudFormationAPI - bearerToken string -} - -// New creates a CloudFormationCallbackAdapter and returns a pointer to the struct. -func New(client cloudformationiface.CloudFormationAPI, bearerToken string) *CloudFormationCallbackAdapter { - return &CloudFormationCallbackAdapter{ - client: client, - bearerToken: bearerToken, - logger: logging.New("callback"), - } -} - -// ReportStatus reports the status back to the Cloudformation service of a handler -// that has moved from Pending to In_Progress -func (c *CloudFormationCallbackAdapter) ReportStatus(operationStatus Status, model []byte, message string, errCode string) error { - if err := c.reportProgress(errCode, operationStatus, InProgress, model, message); err != nil { - return err - } - return nil -} - -// ReportInitialStatus reports the initial status back to the Cloudformation service. -func (c *CloudFormationCallbackAdapter) ReportInitialStatus() error { - if err := c.reportProgress("", InProgress, Pending, []byte(""), ""); err != nil { - return err - } - return nil -} - -// ReportFailureStatus reports the failure status back to the Cloudformation service. -func (c *CloudFormationCallbackAdapter) ReportFailureStatus(model []byte, errCode string, handlerError error) error { - if err := c.reportProgress(errCode, Failed, InProgress, model, handlerError.Error()); err != nil { - return err - } - return nil -} - -// ReportProgress reports the current status back to the Cloudformation service. -func (c *CloudFormationCallbackAdapter) reportProgress(code string, operationStatus Status, currentOperationStatus Status, resourceModel []byte, statusMessage string) error { - - in := cloudformation.RecordHandlerProgressInput{ - BearerToken: aws.String(c.bearerToken), - OperationStatus: aws.String(TranslateOperationStatus(operationStatus)), - } - - if len(statusMessage) != 0 { - in.SetStatusMessage(statusMessage) - } - - if len(resourceModel) != 0 { - in.SetResourceModel(string(resourceModel)) - } - - if len(code) != 0 { - in.SetErrorCode(TranslateErrorCode(code)) - } - - if len(currentOperationStatus) != 0 { - in.SetCurrentOperationStatus(string(currentOperationStatus)) - } - - c.logger.Printf("Record progress: %v", &in) - - return nil -} - -// TranslateErrorCode : Translate the error code into a standard Cloudformation error -func TranslateErrorCode(errorCode string) string { - switch errorCode { - case cloudformation.HandlerErrorCodeNotUpdatable, - cloudformation.HandlerErrorCodeInvalidRequest, - cloudformation.HandlerErrorCodeAccessDenied, - cloudformation.HandlerErrorCodeInvalidCredentials, - cloudformation.HandlerErrorCodeAlreadyExists, - cloudformation.HandlerErrorCodeNotFound, - cloudformation.HandlerErrorCodeResourceConflict, - cloudformation.HandlerErrorCodeThrottling, - cloudformation.HandlerErrorCodeServiceLimitExceeded, - cloudformation.HandlerErrorCodeNotStabilized, - cloudformation.HandlerErrorCodeGeneralServiceException, - cloudformation.HandlerErrorCodeServiceInternalError, - cloudformation.HandlerErrorCodeNetworkFailure, - cloudformation.HandlerErrorCodeInternalFailure: - return errorCode - default: - // InternalFailure is CloudFormation's fallback error code when no more specificity is there - return cloudformation.HandlerErrorCodeInternalFailure - } -} - -// TranslateOperationStatus Translate the operation Status into a standard Cloudformation error -func TranslateOperationStatus(operationStatus Status) string { - - switch operationStatus { - case Success: - return cloudformation.OperationStatusSuccess - case Failed: - return cloudformation.OperationStatusFailed - case InProgress: - return cloudformation.OperationStatusInProgress - case Pending: - return cloudformation.OperationStatusPending - default: - // default will be to fail on unknown status - return cloudformation.OperationStatusFailed - } - -} diff --git a/cfn/callback/callback_test.go b/cfn/callback/callback_test.go index 1c929316..5249f3a8 100644 --- a/cfn/callback/callback_test.go +++ b/cfn/callback/callback_test.go @@ -1,55 +1,48 @@ package callback import ( + "context" "errors" "testing" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" - "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" + "github.com/aws/aws-sdk-go-v2/service/cloudformation" + cftypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" ) var MockModel = []byte("{\"foo\": \"bar\"}") -// MockedEvents mocks the call to AWS CloudWatch Events type MockedCallback struct { - cloudformationiface.CloudFormationAPI errCount int } func NewMockedCallback(errCount int) *MockedCallback { - return &MockedCallback{ - errCount: errCount, - } + return &MockedCallback{errCount: errCount} } -func (m *MockedCallback) RecordHandlerProgress(in *cloudformation.RecordHandlerProgressInput) (*cloudformation.RecordHandlerProgressOutput, error) { - +func (m *MockedCallback) RecordHandlerProgress(ctx context.Context, in *cloudformation.RecordHandlerProgressInput, optFns ...func(*cloudformation.Options)) (*cloudformation.RecordHandlerProgressOutput, error) { if m.errCount > 0 { m.errCount-- return nil, errors.New("error") } - return nil, nil } func TestTranslateOperationStatus(t *testing.T) { - type args struct { - operationStatus Status - } tests := []struct { name string - args args + in Status want string }{ - {"TestSUCCESS", args{"SUCCESS"}, cloudformation.OperationStatusSuccess}, - {"TestFAILED", args{"FAILED"}, cloudformation.OperationStatusFailed}, - {"TestIN_PROGRESS", args{"IN_PROGRESS"}, cloudformation.OperationStatusInProgress}, - {"TestFoo", args{"Foo"}, cloudformation.OperationStatusFailed}, + {"SUCCESS", Success, string(cftypes.OperationStatusSuccess)}, + {"FAILED", Failed, string(cftypes.OperationStatusFailed)}, + {"IN_PROGRESS", InProgress, string(cftypes.OperationStatusInProgress)}, + {"PENDING", Pending, string(cftypes.OperationStatusPending)}, + {"Unknown", "Foo", string(cftypes.OperationStatusFailed)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := TranslateOperationStatus(tt.args.operationStatus); got != tt.want { + if got := TranslateOperationStatus(tt.in); got != tt.want { t.Errorf("TranslateOperationStatus() = %v, want %v", got, tt.want) } }) @@ -57,32 +50,29 @@ func TestTranslateOperationStatus(t *testing.T) { } func TestTranslateErrorCode(t *testing.T) { - type args struct { - errorCode string - } tests := []struct { name string - args args + in string want string }{ - {"TestNotUpdatable", args{"NotUpdatable"}, cloudformation.HandlerErrorCodeNotUpdatable}, - {"TestInvalidRequest", args{"InvalidRequest"}, cloudformation.HandlerErrorCodeInvalidRequest}, - {"AccessDenied", args{"AccessDenied"}, cloudformation.HandlerErrorCodeAccessDenied}, - {"TestInvalidCredentials", args{"InvalidCredentials"}, cloudformation.HandlerErrorCodeInvalidCredentials}, - {"TestAlreadyExists", args{"AlreadyExists"}, cloudformation.HandlerErrorCodeAlreadyExists}, - {"TestNotFound", args{"NotFound"}, cloudformation.HandlerErrorCodeNotFound}, - {"TestResourceConflict", args{"ResourceConflict"}, cloudformation.HandlerErrorCodeResourceConflict}, - {"TestThrottling", args{"Throttling"}, cloudformation.HandlerErrorCodeThrottling}, - {"TestServiceLimitExceeded", args{"ServiceLimitExceeded"}, cloudformation.HandlerErrorCodeServiceLimitExceeded}, - {"TestGeneralServiceException", args{"GeneralServiceException"}, cloudformation.HandlerErrorCodeGeneralServiceException}, - {"TestServiceInternalError", args{"ServiceInternalError"}, cloudformation.HandlerErrorCodeServiceInternalError}, - {"TestNetworkFailure", args{"NetworkFailure"}, cloudformation.HandlerErrorCodeNetworkFailure}, - {"TestFoo", args{"foo"}, cloudformation.HandlerErrorCodeInternalFailure}, - {"TestInternalFailure", args{"InternalFailure"}, cloudformation.HandlerErrorCodeInternalFailure}, + {"NotUpdatable", "NotUpdatable", string(cftypes.HandlerErrorCodeNotUpdatable)}, + {"InvalidRequest", "InvalidRequest", string(cftypes.HandlerErrorCodeInvalidRequest)}, + {"AccessDenied", "AccessDenied", string(cftypes.HandlerErrorCodeAccessDenied)}, + {"InvalidCredentials", "InvalidCredentials", string(cftypes.HandlerErrorCodeInvalidCredentials)}, + {"AlreadyExists", "AlreadyExists", string(cftypes.HandlerErrorCodeAlreadyExists)}, + {"NotFound", "NotFound", string(cftypes.HandlerErrorCodeNotFound)}, + {"ResourceConflict", "ResourceConflict", string(cftypes.HandlerErrorCodeResourceConflict)}, + {"Throttling", "Throttling", string(cftypes.HandlerErrorCodeThrottling)}, + {"ServiceLimitExceeded", "ServiceLimitExceeded", string(cftypes.HandlerErrorCodeServiceLimitExceeded)}, + {"GeneralServiceException", "GeneralServiceException", string(cftypes.HandlerErrorCodeGeneralServiceException)}, + {"ServiceInternalError", "ServiceInternalError", string(cftypes.HandlerErrorCodeServiceInternalError)}, + {"NetworkFailure", "NetworkFailure", string(cftypes.HandlerErrorCodeNetworkFailure)}, + {"InternalFailure", "InternalFailure", string(cftypes.HandlerErrorCodeInternalFailure)}, + {"Unknown", "foo", string(cftypes.HandlerErrorCodeInternalFailure)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := TranslateErrorCode(tt.args.errorCode); got != tt.want { + if got := TranslateErrorCode(tt.in); got != tt.want { t.Errorf("TranslateErrorCode() = %v, want %v", got, tt.want) } }) @@ -90,35 +80,12 @@ func TestTranslateErrorCode(t *testing.T) { } func TestCloudFormationCallbackAdapterReportProgress(t *testing.T) { - type fields struct { - client cloudformationiface.CloudFormationAPI - } - type args struct { - bearerToken string - code string - status Status - operationStatus Status - resourceModel []byte - statusMessage string - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - {"TestRetryReturnNoErr", fields{NewMockedCallback(0)}, args{"123456", "ACCESSDENIED", "FAILED", "IN_PROGRESS", MockModel, "retry"}, false}, + c := &CloudFormationCallbackAdapter{ + client: NewMockedCallback(0), + logger: logging.New("callback: "), + bearerToken: "123456", } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &CloudFormationCallbackAdapter{ - client: tt.fields.client, - logger: logging.New("callback: "), - bearerToken: tt.args.bearerToken, - } - if err := c.reportProgress(tt.args.code, tt.args.status, tt.args.operationStatus, tt.args.resourceModel, tt.args.statusMessage); (err != nil) != tt.wantErr { - t.Errorf("CloudFormationCallbackAdapter.ReportProgress() error = %v, wantErr %v", err, tt.wantErr) - } - }) + if err := c.reportProgress("AccessDenied", Failed, InProgress, MockModel, "retry"); err != nil { + t.Errorf("reportProgress() error = %v", err) } } diff --git a/cfn/cfn.go b/cfn/cfn.go index 2079790e..df6a83c3 100644 --- a/cfn/cfn.go +++ b/cfn/cfn.go @@ -3,6 +3,7 @@ package cfn import ( "context" "errors" + "io" "log" "os" "sync" @@ -15,8 +16,8 @@ import ( "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" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" ) const ( @@ -40,12 +41,9 @@ const ( var once sync.Once -// Handler is the interface that all resource providers must implement +// 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 @@ -54,52 +52,51 @@ type Handler interface { 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. +// Start is the entry point called from a resource's main function. func Start(h Handler) { defer func() { if r := recover(); r != nil { log.Printf("Handler panicked: %s", r) - panic(r) // Continue the panic + panic(r) } }() 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) + providerCfg := credentials.ConfigFromCredentialsProvider(&event.RequestData.ProviderCredentials) + m := metrics.New(cloudwatch.NewFromConfig(providerCfg), 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) + var l io.Writer + if event.RequestData.ProviderLogGroupName == "" { + log.Printf("No ProviderLogGroupName in event, logging to stdout") l = os.Stdout + } else { + var err error + l, err = logging.NewCloudWatchLogsProvider( + cloudwatchlogs.NewFromConfig(providerCfg), + event.RequestData.ProviderLogGroupName, + ) + if err != nil { + log.Printf("Error setting up CloudWatch Logs: %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) @@ -110,6 +107,7 @@ func makeEventFunc(h Handler) eventFunc { if err := validateEvent(event); err != nil { return re.report(event, "validation error", err, invalidRequestError) } + rctx := handler.RequestContext{ StackID: event.StackID, Region: event.Region, @@ -118,15 +116,19 @@ func makeEventFunc(h Handler) eventFunc { SystemTags: event.RequestData.SystemTags, NextToken: event.NextToken, } + + callerCfg := credentials.ConfigFromCredentialsProvider(&event.RequestData.CallerCredentials) + request := handler.NewRequest( event.RequestData.LogicalResourceID, event.CallbackContext, rctx, - credentials.SessionFromCredentialsProvider(&event.RequestData.CallerCredentials), + &callerCfg, 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 { @@ -140,10 +142,7 @@ func makeEventFunc(h Handler) eventFunc { } } -// 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 @@ -156,29 +155,20 @@ func router(a string, h Handler) (handlerFunc, cfnerr.Error) { 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, - ) + 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 @@ -188,12 +178,9 @@ func invoke(handlerFn handlerFunc, request handler.Request, metricsPublisher *me func isMutatingAction(action string) bool { switch action { - case createAction: - return true - case updateAction: - return true - case deleteAction: + case createAction, updateAction, deleteAction: return true } return false } + diff --git a/cfn/cfn_test.go b/cfn/cfn_test.go index 1c30e06c..c49952fb 100644 --- a/cfn/cfn_test.go +++ b/cfn/cfn_test.go @@ -13,9 +13,8 @@ import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" "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" + "github.com/aws/aws-sdk-go-v2/aws" + cftypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" ) func TestMakeEventFunc(t *testing.T) { @@ -23,16 +22,15 @@ func TestMakeEventFunc(t *testing.T) { future := start.Add(time.Minute * 15) tc, cancel := context.WithDeadline(context.Background(), future) - defer cancel() lc := lambdacontext.NewContext(tc, &lambdacontext.LambdaContext{}) - f1 := func(callback map[string]interface{}, s *session.Session) handler.ProgressEvent { + f1 := func(callback map[string]interface{}, cfg *aws.Config) handler.ProgressEvent { return handler.ProgressEvent{} } - f2 := func(callback map[string]interface{}, s *session.Session) handler.ProgressEvent { + f2 := func(callback map[string]interface{}, cfg *aws.Config) handler.ProgressEvent { return handler.ProgressEvent{ OperationStatus: handler.InProgress, Message: "In Progress", @@ -40,21 +38,19 @@ func TestMakeEventFunc(t *testing.T) { } } - f3 := func(callback map[string]interface{}, s *session.Session) handler.ProgressEvent { + f3 := func(callback map[string]interface{}, cfg *aws.Config) handler.ProgressEvent { return handler.ProgressEvent{ OperationStatus: handler.Failed, } } - f4 := func(callback map[string]interface{}, s *session.Session) (response handler.ProgressEvent) { + f4 := func(callback map[string]interface{}, cfg *aws.Config) (response handler.ProgressEvent) { defer func() { - // Catch any panics and return a failed ProgressEvent if r := recover(); r != nil { err, ok := r.(error) if !ok { err = errors.New(fmt.Sprint(r)) } - response = handler.NewFailedEvent(err) } }() @@ -96,7 +92,7 @@ func TestMakeEventFunc(t *testing.T) { }, true}, {"Test wrap panic", args{&MockHandler{f4}, context.Background(), loadEvent("request.create.json", &event{})}, response{ OperationStatus: handler.Failed, - ErrorCode: cloudformation.HandlerErrorCodeGeneralServiceException, + ErrorCode: string(cftypes.HandlerErrorCodeGeneralServiceException), Message: "Unable to complete request: error", BearerToken: "123456", }, false}, @@ -104,7 +100,6 @@ func TestMakeEventFunc(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := makeEventFunc(tt.args.h) - got, err := f(tt.args.ctx, tt.args.event) if (err != nil) != tt.wantErr { @@ -117,25 +112,20 @@ func TestMakeEventFunc(t *testing.T) { if tt.want.OperationStatus != got.OperationStatus { t.Errorf("response = %v; want %v", got.OperationStatus, tt.want.OperationStatus) } - case false: if !reflect.DeepEqual(tt.want, got) { t.Errorf("response = %v; want %v", got, tt.want) } - } - }) } } -// loadEvent is a helper function that unmarshal the event from a file. func loadEvent(path string, evt *event) *event { validevent, err := openFixture(path) if err != nil { log.Fatalf("Unable to read fixture: %v", err) } - if err := json.Unmarshal(validevent, evt); err != nil { log.Fatalf("Marshaling error with event: %v", err) } @@ -148,6 +138,7 @@ func TestMakeEventFuncModel(t *testing.T) { tc, cancel := context.WithDeadline(context.Background(), future) defer cancel() lc := lambdacontext.NewContext(tc, &lambdacontext.LambdaContext{}) + f1 := func(r handler.Request) handler.ProgressEvent { m := MockModel{} if len(r.CallbackContext) == 1 { @@ -172,6 +163,7 @@ func TestMakeEventFuncModel(t *testing.T) { ResourceModel: &m, } } + type args struct { h Handler ctx context.Context @@ -199,14 +191,13 @@ func TestMakeEventFuncModel(t *testing.T) { if err != nil { t.Errorf("TestMakeEventFuncModel() = %v", err) } - wantrModel, err := encoding.Stringify(tt.want) + wantModel, err := encoding.Stringify(tt.want) if err != nil { t.Errorf("TestMakeEventFuncModel() = %v", err) } - if wantrModel != model { - t.Errorf("response = %v; want %v", model, wantrModel) + if wantModel != model { + t.Errorf("response = %v; want %v", model, wantModel) } - }) } } diff --git a/cfn/context.go b/cfn/context.go index 178f18f4..4384d7fd 100644 --- a/cfn/context.go +++ b/cfn/context.go @@ -5,20 +5,15 @@ import ( "fmt" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go-v2/aws" ) -// contextKey is used to prevent collisions within the context package -// and to guarantee returning the correct values from a context type contextKey string - -// callbackContextValues is used to guarantee the type of -// values stored in the context type callbackContextValues map[string]interface{} const ( valuesKey = contextKey("user_callback_context") - sessionKey = contextKey("aws_session") + configKey = contextKey("aws_config") ) // SetContextValues creates a context to pass to handlers @@ -32,22 +27,20 @@ func GetContextValues(ctx context.Context) (map[string]interface{}, error) { if !ok { return nil, fmt.Errorf("Values not found") } - return map[string]interface{}(values), nil } -// SetContextSession adds the supplied session to the given context -func SetContextSession(ctx context.Context, sess *session.Session) context.Context { - return context.WithValue(ctx, sessionKey, sess) +// SetContextConfig adds the supplied AWS config to the given context +func SetContextConfig(ctx context.Context, cfg *aws.Config) context.Context { + return context.WithValue(ctx, configKey, cfg) } -// GetContextSession unwraps a session from a given context -func GetContextSession(ctx context.Context) (*session.Session, error) { - val, ok := ctx.Value(sessionKey).(*session.Session) +// GetContextConfig unwraps an aws.Config from a given context +func GetContextConfig(ctx context.Context) (*aws.Config, error) { + val, ok := ctx.Value(configKey).(*aws.Config) if !ok { - return nil, fmt.Errorf("Session not found") + return nil, fmt.Errorf("Config not found") } - return val, nil } diff --git a/cfn/credentials/credentials.go b/cfn/credentials/credentials.go index 24963f44..c3aeb920 100644 --- a/cfn/credentials/credentials.go +++ b/cfn/credentials/credentials.go @@ -1,76 +1,51 @@ /* -Package credentials providers helper functions for dealing with AWS credentials +Package credentials provides helper functions for dealing with AWS credentials passed in to resource providers from CloudFormation. */ package credentials import ( - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" -) - -// CloudFormationCredentialsProviderName ... -const CloudFormationCredentialsProviderName = "CloudFormationCredentialsProvider" + "context" -const InvalidSessionError = "InvalidSession" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" +) -// NewProvider ... -func NewProvider(accessKeyID string, secretAccessKey string, sessionToken string) credentials.Provider { - return &CloudFormationCredentialsProvider{ - AccessKeyID: accessKeyID, - SecretAccessKey: secretAccessKey, - SessionToken: sessionToken, - } -} +const ( + // CloudFormationCredentialsProviderName is the name of the credentials provider. + CloudFormationCredentialsProviderName = "CloudFormationCredentialsProvider" + // InvalidSessionError is returned when a session is invalid. + InvalidSessionError = "InvalidSession" +) -// CloudFormationCredentialsProvider ... +// CloudFormationCredentialsProvider holds the credentials passed by CloudFormation. type CloudFormationCredentialsProvider struct { - retrieved bool - - // AccessKeyID ... - AccessKeyID string `json:"accessKeyId"` - - // SecretAccessKey ... - SecretAccessKey string `json:"secretAccessKey"` - - // SessionToken ... - SessionToken string `json:"sessionToken"` + AccessKeyID string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + SessionToken string `json:"sessionToken"` } -// Retrieve ... -func (c *CloudFormationCredentialsProvider) Retrieve() (credentials.Value, error) { - c.retrieved = false - - value := credentials.Value{ +// Retrieve implements aws.CredentialsProvider. +func (c *CloudFormationCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { + return aws.Credentials{ AccessKeyID: c.AccessKeyID, - SecretAccessKey: c.SecretAccessKey, + SecretAccessKey: c.SecretAccessKey, SessionToken: c.SessionToken, - ProviderName: CloudFormationCredentialsProviderName, - } - - c.retrieved = true - - return value, nil + Source: CloudFormationCredentialsProviderName, + }, nil } -// IsExpired ... -func (c *CloudFormationCredentialsProvider) IsExpired() bool { - return false -} - -// SessionFromCredentialsProvider creates a new AWS SDK session from a credentials provider -// -// A credentials provider is an interface in the AWS SDK's credentials package (aws/credentials) -// We transform it into a session for later use in the RPDK -func SessionFromCredentialsProvider(provider credentials.Provider) *session.Session { - creds := credentials.NewCredentials(provider) - - sess := session.Must(session.NewSessionWithOptions(session.Options{ - Config: aws.Config{ - Credentials: creds, - }, - })) - - return sess +// ConfigFromCredentialsProvider creates an aws.Config from a credentials provider. +func ConfigFromCredentialsProvider(provider *CloudFormationCredentialsProvider) aws.Config { + cfg, _ := config.LoadDefaultConfig(context.Background(), + config.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider( + provider.AccessKeyID, + provider.SecretAccessKey, + provider.SessionToken, + ), + ), + ) + return cfg } diff --git a/cfn/credentials/credentials_test.go b/cfn/credentials/credentials_test.go index 4684545b..384f7b62 100644 --- a/cfn/credentials/credentials_test.go +++ b/cfn/credentials/credentials_test.go @@ -1,36 +1,49 @@ package credentials -import "testing" +import ( + "context" + "testing" +) func TestCredentials(t *testing.T) { - t.Run("New", func(t *testing.T) { - creds := NewProvider("a", "b", "c") + t.Run("Retrieve", func(t *testing.T) { + creds := &CloudFormationCredentialsProvider{ + AccessKeyID: "a", + SecretAccessKey: "b", + SessionToken: "c", + } - val, err := creds.Retrieve() + val, err := creds.Retrieve(context.Background()) if err != nil { t.Fatalf("Unable to retrieve credentials: %v", err) } if val.AccessKeyID != "a" { t.Fatalf("Incorrect access key: %v", val.AccessKeyID) } - }) - - t.Run("Expired", func(t *testing.T) { - creds := NewProvider("a", "b", "c") - - if creds.IsExpired() != false { - t.Fatalf("Credentials should never expire") + if val.SecretAccessKey != "b" { + t.Fatalf("Incorrect secret key: %v", val.SecretAccessKey) + } + if val.SessionToken != "c" { + t.Fatalf("Incorrect session token: %v", val.SessionToken) } }) } -func TestSessionFromCredentialsProvider(t *testing.T) { +func TestConfigFromCredentialsProvider(t *testing.T) { t.Run("Happy Path", func(t *testing.T) { - creds := NewProvider("a", "b", "c") - sess := SessionFromCredentialsProvider(creds) + provider := &CloudFormationCredentialsProvider{ + AccessKeyID: "a", + SecretAccessKey: "b", + SessionToken: "c", + } + cfg := ConfigFromCredentialsProvider(provider) - if sess == nil { - t.Fatalf("Unable to create session") + creds, err := cfg.Credentials.Retrieve(context.Background()) + if err != nil { + t.Fatalf("Unable to retrieve credentials from config: %v", err) + } + if creds.AccessKeyID != "a" { + t.Fatalf("Incorrect access key: %v", creds.AccessKeyID) } }) } diff --git a/cfn/encoding/encoding_test.go b/cfn/encoding/encoding_test.go index d35fd379..6ce2a0bd 100644 --- a/cfn/encoding/encoding_test.go +++ b/cfn/encoding/encoding_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" ) func TestEncoding(t *testing.T) { @@ -87,7 +87,7 @@ func TestEncoding(t *testing.T) { // And check it matches the expected form if diff := cmp.Diff(jsonTest, stringMap); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } // Now check we can get the original struct back @@ -98,6 +98,6 @@ func TestEncoding(t *testing.T) { } if diff := cmp.Diff(m, b); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } } diff --git a/cfn/encoding/marshal_test.go b/cfn/encoding/marshal_test.go index d4c0a961..bdaff29b 100644 --- a/cfn/encoding/marshal_test.go +++ b/cfn/encoding/marshal_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" ) func TestMarshaling(t *testing.T) { @@ -87,7 +87,7 @@ func TestMarshaling(t *testing.T) { // And check it matches the expected form if diff := cmp.Diff(jsonTest, stringMap); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } // Now check we can get the original struct back @@ -98,6 +98,6 @@ func TestMarshaling(t *testing.T) { } if diff := cmp.Diff(m, b); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } } diff --git a/cfn/encoding/stringify_test.go b/cfn/encoding/stringify_test.go index 8387222f..b271de44 100644 --- a/cfn/encoding/stringify_test.go +++ b/cfn/encoding/stringify_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/google/go-cmp/cmp" ) @@ -61,7 +61,7 @@ func TestStringifyTypes(t *testing.T) { } if d := cmp.Diff(actual, testCase.expected); d != "" { - t.Errorf(d) + t.Errorf("%s", d) } } } @@ -110,6 +110,6 @@ func TestStringifyModel(t *testing.T) { } if d := cmp.Diff(actual, expected); d != "" { - t.Errorf(d) + t.Errorf("%s", d) } } diff --git a/cfn/encoding/unstringify_test.go b/cfn/encoding/unstringify_test.go index 8907853d..acba26c4 100644 --- a/cfn/encoding/unstringify_test.go +++ b/cfn/encoding/unstringify_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/google/go-cmp/cmp" ) diff --git a/cfn/handler/event.go b/cfn/handler/event.go index 9eed837c..d93b76a9 100644 --- a/cfn/handler/event.go +++ b/cfn/handler/event.go @@ -2,7 +2,7 @@ package handler import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" - "github.com/aws/aws-sdk-go/service/cloudformation" + cftypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" ) // ProgressEvent represent the progress of CRUD handlers. @@ -45,7 +45,7 @@ type ProgressEvent struct { } // NewProgressEvent creates a new event with -// a default OperationStatus of Unkown +// a default OperationStatus of Unknown func NewProgressEvent() ProgressEvent { return ProgressEvent{ OperationStatus: UnknownStatus, @@ -56,7 +56,7 @@ func NewProgressEvent() ProgressEvent { // based on the error passed in. func NewFailedEvent(err error) ProgressEvent { cerr := cfnerr.New( - cloudformation.HandlerErrorCodeGeneralServiceException, + string(cftypes.HandlerErrorCodeGeneralServiceException), "Unable to complete request: "+err.Error(), err, ) @@ -64,6 +64,6 @@ func NewFailedEvent(err error) ProgressEvent { return ProgressEvent{ OperationStatus: Failed, Message: cerr.Message(), - HandlerErrorCode: cloudformation.HandlerErrorCodeGeneralServiceException, + HandlerErrorCode: string(cftypes.HandlerErrorCodeGeneralServiceException), } } diff --git a/cfn/handler/event_test.go b/cfn/handler/event_test.go index 82af0926..bcb50576 100644 --- a/cfn/handler/event_test.go +++ b/cfn/handler/event_test.go @@ -1,13 +1,12 @@ package handler import ( + "encoding/json" "testing" - "github.com/aws/aws-sdk-go/service/cloudformation" + cftypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/google/go-cmp/cmp" - "encoding/json" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" ) @@ -31,7 +30,7 @@ func TestProgressEventMarshalJSON(t *testing.T) { Name: encoding.NewString("Douglas"), Version: encoding.NewFloat(42.1), }, - HandlerErrorCode: cloudformation.HandlerErrorCodeNotUpdatable, + HandlerErrorCode: string(cftypes.HandlerErrorCodeNotUpdatable), }, expected: `{"status":"FAILED","errorCode":"NotUpdatable","message":"foo","resourceModel":{"Name":"Douglas","Version":"42.1"},"resourceModels":null}`, }, @@ -58,16 +57,14 @@ func TestProgressEventMarshalJSON(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - actual, err := json.Marshal(tt.event) if err != nil { t.Errorf("Unexpected error marshaling event JSON: %s", err) } if diff := cmp.Diff(string(actual), tt.expected); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } }) } - } diff --git a/cfn/handler/handler_test.go b/cfn/handler/handler_test.go index 1415af11..4ead16d7 100644 --- a/cfn/handler/handler_test.go +++ b/cfn/handler/handler_test.go @@ -3,7 +3,7 @@ package handler import ( "testing" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" ) type Props struct { @@ -22,7 +22,7 @@ func TestNewRequest(t *testing.T) { t.Fatalf("Unable to unmarshal props: %v", err) } - if aws.StringValue(prev.Color) != "red" { + if aws.ToString(prev.Color) != "red" { t.Fatalf("Previous Properties don't match: %v", prev.Color) } @@ -30,14 +30,13 @@ func TestNewRequest(t *testing.T) { t.Fatalf("Unable to unmarshal props: %v", err) } - if aws.StringValue(curr.Color) != "green" { + if aws.ToString(curr.Color) != "green" { t.Fatalf("Properties don't match: %v", curr.Color) } if req.LogicalResourceID != "foo" { t.Fatalf("Invalid Logical Resource ID: %v", req.LogicalResourceID) } - }) t.Run("ResourceProps", func(t *testing.T) { diff --git a/cfn/handler/request.go b/cfn/handler/request.go index c768e73e..7ff6601a 100644 --- a/cfn/handler/request.go +++ b/cfn/handler/request.go @@ -1,18 +1,15 @@ package handler import ( - "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" ) const ( - // marshalingError occurs when we can't marshal data from one format into another. marshalingError = "Marshaling" - - // bodyEmptyError happens when the resource body is empty - bodyEmptyError = "BodyEmpty" + bodyEmptyError = "BodyEmpty" ) // Request is passed to actions with customer related data @@ -27,12 +24,11 @@ type Request struct { // identifier which can be used to continue polling for stabilization CallbackContext map[string]interface{} - // The RequestContext is information about the current - // invocation. + // The RequestContext is information about the current invocation. RequestContext RequestContext - // An authenticated AWS session that can be used with the AWS Go SDK - Session *session.Session + // Config is an AWS SDK v2 config authenticated with the caller's credentials. + Config *aws.Config previousResourcePropertiesBody []byte resourcePropertiesBody []byte @@ -42,31 +38,20 @@ type Request struct { // RequestContext represents information about the current // invocation request of the handler. type RequestContext struct { - // The stack ID of the CloudFormation stack - StackID string - - // The Region of the requester - Region string - - // The Account ID of the requester - AccountID string - - // The stack tags associated with the cloudformation stack - StackTags map[string]string - - // The SystemTags associated with the request + StackID string + Region string + AccountID string + StackTags map[string]string SystemTags map[string]string - - // The NextToken provided in the request - NextToken string + NextToken string } // 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, typeConfig []byte) Request { +func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext, cfg *aws.Config, previousBody, body, typeConfig []byte) Request { return Request{ LogicalResourceID: id, CallbackContext: ctx, - Session: sess, + Config: cfg, previousResourcePropertiesBody: previousBody, resourcePropertiesBody: body, RequestContext: requestCTX, diff --git a/cfn/handler/request_test.go b/cfn/handler/request_test.go index f7728fd2..a1564d1c 100644 --- a/cfn/handler/request_test.go +++ b/cfn/handler/request_test.go @@ -3,7 +3,7 @@ package handler import ( "testing" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/google/go-cmp/cmp" ) @@ -51,7 +51,7 @@ func TestUnmarshal(t *testing.T) { t.Error(err) } if diff := cmp.Diff(actual, expectedPrevious); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } // Current body @@ -60,6 +60,6 @@ func TestUnmarshal(t *testing.T) { t.Error(err) } if diff := cmp.Diff(actual, expectedCurrent); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } } diff --git a/cfn/logging/cloudwatchlogs.go b/cfn/logging/cloudwatchlogs.go index ddd78daf..d612c576 100644 --- a/cfn/logging/cloudwatchlogs.go +++ b/cfn/logging/cloudwatchlogs.go @@ -1,69 +1,63 @@ package logging import ( + "context" "io" "log" "os" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + cwltypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" "github.com/segmentio/ksuid" ) -// NewCloudWatchLogsProvider creates a io.Writer that writes -// to a specifc log group. +// CloudWatchLogsClient is the subset of the CloudWatch Logs API used by this package. +type CloudWatchLogsClient interface { + DescribeLogGroups(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) + CreateLogGroup(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) + CreateLogStream(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) + PutLogEvents(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) +} + +// NewCloudWatchLogsProvider creates an io.Writer that writes to a specific log group. // // Each time NewCloudWatchLogsProvider is used, a new log stream is created -// inside the log group. The log stream will have a unique, random identifer -// -// sess := session.Must(aws.NewConfig()) -// svc := cloudwatchlogs.New(sess) -// -// provider, err := NewCloudWatchLogsProvider(svc, "pineapple-pizza") -// if err != nil { -// panic(err) -// } -// -// // set log output to the provider, all log messages will then be -// // pushed through the Write func and sent to CloudWatch Logs -// log.SetOutput(provider) -// log.Printf("Eric loves pineapple pizza!") -func NewCloudWatchLogsProvider(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string) (io.Writer, error) { +// inside the log group. +func NewCloudWatchLogsProvider(client CloudWatchLogsClient, logGroupName string) (io.Writer, error) { logger := New("internal: ") - // If we're running in SAM CLI, we can return the stdout + // If running in SAM CLI, return stderr. if len(os.Getenv("AWS_SAM_LOCAL")) > 0 && len(os.Getenv("AWS_FORCE_INTEGRATIONS")) == 0 { return stdErr, nil } - ok, err := CloudWatchLogGroupExists(client, logGroupName) + ctx := context.Background() + + ok, err := cloudWatchLogGroupExists(ctx, client, logGroupName) if err != nil { return nil, err } if !ok { logger.Printf("Need to create loggroup: %v", logGroupName) - if err := CreateNewCloudWatchLogGroup(client, logGroupName); err != nil { + if err := createNewCloudWatchLogGroup(ctx, client, logGroupName); err != nil { return nil, err } } - logStreamName := ksuid.New() - // need to create logstream - if err := CreateNewLogStream(client, logGroupName, logStreamName.String()); err != nil { + logStreamName := ksuid.New().String() + if err := createNewLogStream(ctx, client, logGroupName, logStreamName); err != nil { return nil, err } provider := &cloudWatchLogsProvider{ - client: client, - + client: client, logGroupName: logGroupName, - logStreamName: logStreamName.String(), - - logger: logger, + logStreamName: logStreamName, + logger: logger, } if _, err := provider.Write([]byte("Initialization of log stream")); err != nil { @@ -74,14 +68,11 @@ func NewCloudWatchLogsProvider(client cloudwatchlogsiface.CloudWatchLogsAPI, log } type cloudWatchLogsProvider struct { - client cloudwatchlogsiface.CloudWatchLogsAPI - + client CloudWatchLogsClient logGroupName string logStreamName string - - sequence string - - logger *log.Logger + sequence *string + logger *log.Logger } func (p *cloudWatchLogsProvider) Write(b []byte) (int, error) { @@ -90,90 +81,55 @@ func (p *cloudWatchLogsProvider) Write(b []byte) (int, error) { input := &cloudwatchlogs.PutLogEventsInput{ LogGroupName: aws.String(p.logGroupName), LogStreamName: aws.String(p.logStreamName), - - LogEvents: []*cloudwatchlogs.InputLogEvent{ + LogEvents: []cwltypes.InputLogEvent{ { Message: aws.String(string(b)), - Timestamp: aws.Int64(time.Now().UnixNano() / int64(time.Millisecond)), + Timestamp: aws.Int64(time.Now().UnixMilli()), }, }, } - if len(p.sequence) != 0 { - input.SetSequenceToken(p.sequence) + if p.sequence != nil { + input.SequenceToken = p.sequence } - resp, err := p.client.PutLogEvents(input) - + resp, err := p.client.PutLogEvents(context.Background(), input) if err != nil { return 0, err } - p.sequence = *resp.NextSequenceToken + p.sequence = resp.NextSequenceToken return len(b), nil } -// CloudWatchLogGroupExists checks if a log group exists -// -// Using the client provided, it will check the CloudWatch Logs -// service to verify the log group -// -// sess := session.Must(aws.NewConfig()) -// svc := cloudwatchlogs.New(sess) -// -// // checks if the pineapple-pizza log group exists -// ok, err := LogGroupExists(svc, "pineapple-pizza") -// if err != nil { -// panic(err) -// } -// if ok { -// // do something -// } -func CloudWatchLogGroupExists(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string) (bool, error) { - resp, err := client.DescribeLogGroups(&cloudwatchlogs.DescribeLogGroupsInput{ - Limit: aws.Int64(1), +func cloudWatchLogGroupExists(ctx context.Context, client CloudWatchLogsClient, logGroupName string) (bool, error) { + resp, err := client.DescribeLogGroups(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ + Limit: aws.Int32(1), LogGroupNamePrefix: aws.String(logGroupName), }) - if err != nil { return false, err } - if len(resp.LogGroups) == 0 || *resp.LogGroups[0].LogGroupName != logGroupName { + if len(resp.LogGroups) == 0 || aws.ToString(resp.LogGroups[0].LogGroupName) != logGroupName { return false, nil } return true, nil } -// CreateNewCloudWatchLogGroup creates a log group in CloudWatch Logs. -// -// Using a passed in client to create the call to the service, it -// will create a log group of the specified name -// -// sess := session.Must(aws.NewConfig()) -// svc := cloudwatchlogs.New(sess) -// -// if err := CreateNewCloudWatchLogGroup(svc, "pineapple-pizza"); err != nil { -// panic("Unable to create log group", err) -// } -func CreateNewCloudWatchLogGroup(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string) error { - if _, err := client.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{ +func createNewCloudWatchLogGroup(ctx context.Context, client CloudWatchLogsClient, logGroupName string) error { + _, err := client.CreateLogGroup(ctx, &cloudwatchlogs.CreateLogGroupInput{ LogGroupName: aws.String(logGroupName), - }); err != nil { - return err - } - - return nil + }) + return err } -// CreateNewLogStream creates a log stream inside of a LogGroup -func CreateNewLogStream(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string, logStreamName string) error { - _, err := client.CreateLogStream(&cloudwatchlogs.CreateLogStreamInput{ +func createNewLogStream(ctx context.Context, client CloudWatchLogsClient, logGroupName, logStreamName string) error { + _, err := client.CreateLogStream(ctx, &cloudwatchlogs.CreateLogStreamInput{ LogGroupName: aws.String(logGroupName), LogStreamName: aws.String(logStreamName), }) - return err } diff --git a/cfn/logging/cloudwatchlogs_test.go b/cfn/logging/cloudwatchlogs_test.go index ce5c435c..4f8899e4 100644 --- a/cfn/logging/cloudwatchlogs_test.go +++ b/cfn/logging/cloudwatchlogs_test.go @@ -1,36 +1,57 @@ package logging import ( + "context" + "errors" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + cwltypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" ) +type mockCWLClient struct { + DescribeLogGroupsFn func(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) + CreateLogGroupFn func(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) + CreateLogStreamFn func(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) + PutLogEventsFn func(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) +} + +func (m *mockCWLClient) DescribeLogGroups(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + return m.DescribeLogGroupsFn(ctx, params, optFns...) +} + +func (m *mockCWLClient) CreateLogGroup(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) { + return m.CreateLogGroupFn(ctx, params, optFns...) +} + +func (m *mockCWLClient) CreateLogStream(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) { + return m.CreateLogStreamFn(ctx, params, optFns...) +} + +func (m *mockCWLClient) PutLogEvents(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) { + return m.PutLogEventsFn(ctx, params, optFns...) +} + func TestCloudWatchLogProvider(t *testing.T) { t.Run("Init", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - DescribeLogGroupsFn: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + client := &mockCWLClient{ + DescribeLogGroupsFn: func(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { return &cloudwatchlogs.DescribeLogGroupsOutput{ - LogGroups: []*cloudwatchlogs.LogGroup{ - {LogGroupName: input.LogGroupNamePrefix}, + LogGroups: []cwltypes.LogGroup{ + {LogGroupName: params.LogGroupNamePrefix}, }, }, nil }, - - CreateLogGroupFn: func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { + CreateLogGroupFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) { return nil, nil }, - - CreateLogStreamFn: func(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { + CreateLogStreamFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) { return nil, nil }, - - PutLogEventsFn: func(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { + PutLogEventsFn: func(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) { return &cloudwatchlogs.PutLogEventsOutput{ - NextSequenceToken: aws.String("zomg"), + NextSequenceToken: aws.String("seq1"), }, nil }, } @@ -42,82 +63,73 @@ func TestCloudWatchLogProvider(t *testing.T) { }) t.Run("Init Error Exists", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - DescribeLogGroupsFn: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { - return nil, awserr.New("Invalid", "Invalid", nil) + client := &mockCWLClient{ + DescribeLogGroupsFn: func(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + return nil, errors.New("access denied") }, - - CreateLogGroupFn: func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { + CreateLogGroupFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) { return nil, nil }, - - CreateLogStreamFn: func(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { + CreateLogStreamFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) { return nil, nil }, - - PutLogEventsFn: func(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { + PutLogEventsFn: func(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) { return &cloudwatchlogs.PutLogEventsOutput{ - NextSequenceToken: aws.String("zomg"), + NextSequenceToken: aws.String("seq1"), }, nil }, } _, err := NewCloudWatchLogsProvider(client, "pineapple-pizza") if err == nil { - t.Fatalf("Error returned: %v", err) + t.Fatalf("Expected error, got nil") } }) t.Run("Init Error Unable to Create", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - DescribeLogGroupsFn: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + client := &mockCWLClient{ + DescribeLogGroupsFn: func(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { return &cloudwatchlogs.DescribeLogGroupsOutput{ - LogGroups: []*cloudwatchlogs.LogGroup{}, + LogGroups: []cwltypes.LogGroup{}, }, nil }, - - CreateLogGroupFn: func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { - return nil, awserr.New("Invalid", "Invalid", nil) + CreateLogGroupFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) { + return nil, errors.New("cannot create log group") }, - - CreateLogStreamFn: func(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { + CreateLogStreamFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) { return nil, nil }, - - PutLogEventsFn: func(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { + PutLogEventsFn: func(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) { return &cloudwatchlogs.PutLogEventsOutput{ - NextSequenceToken: aws.String("zomg"), + NextSequenceToken: aws.String("seq1"), }, nil }, } _, err := NewCloudWatchLogsProvider(client, "pineapple-pizza") if err == nil { - t.Fatalf("Error returned: %v", err) + t.Fatalf("Expected error, got nil") } }) t.Run("Write", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - DescribeLogGroupsFn: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + client := &mockCWLClient{ + DescribeLogGroupsFn: func(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { return &cloudwatchlogs.DescribeLogGroupsOutput{ - LogGroups: []*cloudwatchlogs.LogGroup{ - {LogGroupName: input.LogGroupNamePrefix}, + LogGroups: []cwltypes.LogGroup{ + {LogGroupName: params.LogGroupNamePrefix}, }, }, nil }, - - CreateLogGroupFn: func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { + CreateLogGroupFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) { return nil, nil }, - - CreateLogStreamFn: func(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { + CreateLogStreamFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) { return nil, nil }, - - PutLogEventsFn: func(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { + PutLogEventsFn: func(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) { return &cloudwatchlogs.PutLogEventsOutput{ - NextSequenceToken: aws.String("zomg"), + NextSequenceToken: aws.String("seq1"), }, nil }, } @@ -132,7 +144,6 @@ func TestCloudWatchLogProvider(t *testing.T) { if err != nil { t.Fatalf("Error returned: %v", err) } - if c != len(line) { t.Fatalf("Didn't write the same content") } @@ -140,32 +151,28 @@ func TestCloudWatchLogProvider(t *testing.T) { t.Run("Write Error", func(t *testing.T) { writeCount := 0 - client := CallbackCloudWatchLogs{ - DescribeLogGroupsFn: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { + client := &mockCWLClient{ + DescribeLogGroupsFn: func(ctx context.Context, params *cloudwatchlogs.DescribeLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { return &cloudwatchlogs.DescribeLogGroupsOutput{ - LogGroups: []*cloudwatchlogs.LogGroup{ - {LogGroupName: input.LogGroupNamePrefix}, + LogGroups: []cwltypes.LogGroup{ + {LogGroupName: params.LogGroupNamePrefix}, }, }, nil }, - - CreateLogGroupFn: func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { + CreateLogGroupFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogGroupInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error) { return nil, nil }, - - CreateLogStreamFn: func(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { + CreateLogStreamFn: func(ctx context.Context, params *cloudwatchlogs.CreateLogStreamInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error) { return nil, nil }, - - PutLogEventsFn: func(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { + PutLogEventsFn: func(ctx context.Context, params *cloudwatchlogs.PutLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error) { if writeCount == 0 { writeCount++ return &cloudwatchlogs.PutLogEventsOutput{ - NextSequenceToken: aws.String("zomg"), + NextSequenceToken: aws.String("seq1"), }, nil } - - return nil, awserr.New("Invalid", "Invalid", nil) + return nil, errors.New("put log events failed") }, } @@ -181,84 +188,3 @@ func TestCloudWatchLogProvider(t *testing.T) { } }) } - -func TestCloudWatchLogGroupExists(t *testing.T) { - t.Run("Success", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - DescribeLogGroupsFn: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { - return &cloudwatchlogs.DescribeLogGroupsOutput{ - LogGroups: []*cloudwatchlogs.LogGroup{ - {LogGroupName: input.LogGroupNamePrefix}, - }, - }, nil - }, - } - - if _, err := CloudWatchLogGroupExists(client, "pineapple-pizza"); err != nil { - t.Fatalf("Error returned: %v", err) - } - }) - - t.Run("Error", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - DescribeLogGroupsFn: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { - return nil, awserr.New("Invalid", "Invalid", nil) - }, - } - - if _, err := CloudWatchLogGroupExists(client, "pineapple-pizza"); err == nil { - t.Fatalf("Error not returned") - } - }) -} - -func TestCreateCloudWatchLogGroup(t *testing.T) { - t.Run("Success", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - CreateLogGroupFn: func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { - return nil, nil - }, - } - - if err := CreateNewCloudWatchLogGroup(client, "pineapple-pizza"); err != nil { - t.Fatalf("Error returned: %v", err) - } - }) - - t.Run("Error", func(t *testing.T) { - client := CallbackCloudWatchLogs{ - CreateLogGroupFn: func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { - return nil, awserr.New("Invalid", "Invalid", nil) - }, - } - - if err := CreateNewCloudWatchLogGroup(client, "pineapple-pizza"); err == nil { - t.Fatalf("Error not returned") - } - }) -} - -type CallbackCloudWatchLogs struct { - cloudwatchlogsiface.CloudWatchLogsAPI - - DescribeLogGroupsFn func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) - CreateLogGroupFn func(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) - CreateLogStreamFn func(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) - PutLogEventsFn func(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) -} - -func (cwl CallbackCloudWatchLogs) DescribeLogGroups(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { - return cwl.DescribeLogGroupsFn(input) -} - -func (cwl CallbackCloudWatchLogs) CreateLogGroup(input *cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) { - return cwl.CreateLogGroupFn(input) -} - -func (cwl CallbackCloudWatchLogs) CreateLogStream(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) { - return cwl.CreateLogStreamFn(input) -} - -func (cwl CallbackCloudWatchLogs) PutLogEvents(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) { - return cwl.PutLogEventsFn(input) -} diff --git a/cfn/logging/logging.go b/cfn/logging/logging.go index 4f2b95cc..140f79cf 100644 --- a/cfn/logging/logging.go +++ b/cfn/logging/logging.go @@ -1,8 +1,5 @@ -//go:build logging -// +build logging - /* -Package logging provides support for logging to cloudwatch +Package logging provides support for logging to CloudWatch within resource providers. */ package logging @@ -14,25 +11,19 @@ import ( "syscall" ) -// define a new stdErr since we'll over-write the default stdout/err -// to prevent data leaking into the service account +// stdErr is a reference to stderr that survives any os.Stderr redirections. var stdErr = os.NewFile(uintptr(syscall.Stderr), "/dev/stderr") var providerLogOutput io.Writer -const ( - loggerError = "Logger" -) - -// SetProviderLogOutput ... +// SetProviderLogOutput configures the provider log output writer. +// When set, log messages go to both stderr and the provider writer (CloudWatch Logs). func SetProviderLogOutput(w io.Writer) { - log.SetOutput(w) - providerLogOutput = w } -// New sets up a logger that writes to the stderr +// New sets up a logger that writes to stderr and (if configured) the provider log output. func New(prefix string) *log.Logger { var w io.Writer @@ -42,6 +33,5 @@ func New(prefix string) *log.Logger { w = stdErr } - // we create our own stderr since we're going to nuke the existing one return log.New(w, prefix, log.LstdFlags) } diff --git a/cfn/logging/logging_notag.go b/cfn/logging/logging_notag.go deleted file mode 100644 index 8f61a07e..00000000 --- a/cfn/logging/logging_notag.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !logging -// +build !logging - -package logging - -import ( - "io" - "log" - "os" - "syscall" -) - -// define a new stdErr since we'll over-write the default stdout/err -// to prevent data leaking into the service account -var stdErr = os.NewFile(uintptr(syscall.Stderr), "/dev/stderr") - -// SetProviderLogOutput ... -func SetProviderLogOutput(w io.Writer) { - // no-op -} - -// New sets up a logger that writes to the stderr -func New(prefix string) *log.Logger { - // we create our own stderr since we're going to nuke the existing one - return log.New(os.Stderr, prefix, log.LstdFlags) -} diff --git a/cfn/metrics/noop_publisher.go b/cfn/metrics/noop_publisher.go index 3b736c75..87afc834 100644 --- a/cfn/metrics/noop_publisher.go +++ b/cfn/metrics/noop_publisher.go @@ -1,25 +1,13 @@ package metrics import ( - "log" + "context" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" - "github.com/aws/aws-sdk-go/service/cloudwatch" - "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" ) -func newNoopClient() *noopCloudWatchClient { - return &noopCloudWatchClient{ - logger: logging.New("metrics"), - } -} - -type noopCloudWatchClient struct { - logger *log.Logger - cloudwatchiface.CloudWatchAPI -} +type noopCloudWatchClient struct{} -func (n *noopCloudWatchClient) PutMetricData(input *cloudwatch.PutMetricDataInput) (*cloudwatch.PutMetricDataOutput, error) { - // out implementation doesn't care about the response +func (n *noopCloudWatchClient) PutMetricData(ctx context.Context, params *cloudwatch.PutMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) { return nil, nil } diff --git a/cfn/metrics/publisher.go b/cfn/metrics/publisher.go index bdf2bb58..220579b6 100644 --- a/cfn/metrics/publisher.go +++ b/cfn/metrics/publisher.go @@ -1,6 +1,7 @@ package metrics import ( + "context" "fmt" "log" "os" @@ -8,42 +9,39 @@ import ( "time" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatch" - "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + cwtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" ) const ( - // MetricNameSpaceRoot is the Metric name space root. - MetricNameSpaceRoot = "AWS/CloudFormation" - // MetricNameHanderException is a metric type. - MetricNameHanderException = "HandlerException" - // MetricNameHanderDuration is a metric type. - MetricNameHanderDuration = "HandlerInvocationDuration" - // MetricNameHanderInvocationCount is a metric type. + MetricNameSpaceRoot = "AWS/CloudFormation" + MetricNameHanderException = "HandlerException" + MetricNameHanderDuration = "HandlerInvocationDuration" MetricNameHanderInvocationCount = "HandlerInvocationCount" - // DimensionKeyAcionType is the Action key in the dimension. - DimensionKeyAcionType = "Action" - // DimensionKeyExceptionType is the ExceptionType in the dimension. - DimensionKeyExceptionType = "ExceptionType" - // DimensionKeyResourceType is the ResourceType in the dimension. - DimensionKeyResourceType = "ResourceType" - // ServiceInternalError ... - ServiceInternalError string = "ServiceInternal" + DimensionKeyAcionType = "Action" + DimensionKeyExceptionType = "ExceptionType" + DimensionKeyResourceType = "ResourceType" + ServiceInternalError = "ServiceInternal" ) -// A Publisher represents an object that publishes metrics to AWS Cloudwatch. +// CloudWatchClient is the subset of the CloudWatch API used by this package. +type CloudWatchClient interface { + PutMetricData(ctx context.Context, params *cloudwatch.PutMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) +} + +// Publisher publishes metrics to AWS CloudWatch. type Publisher struct { - client cloudwatchiface.CloudWatchAPI // AWS CloudWatch Service Client - namespace string // custom resouces's namespace + client CloudWatchClient + namespace string logger *log.Logger - resourceType string // type of resource + resourceType string } // New creates a new Publisher. -func New(client cloudwatchiface.CloudWatchAPI, resType string) *Publisher { +func New(client CloudWatchClient, resType string) *Publisher { if len(os.Getenv("AWS_SAM_LOCAL")) > 0 { - client = newNoopClient() + client = &noopCloudWatchClient{} } rn := ResourceTypeName(resType) return &Publisher{ @@ -54,75 +52,61 @@ func New(client cloudwatchiface.CloudWatchAPI, resType string) *Publisher { } } -// 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), + DimensionKeyAcionType: action, DimensionKeyExceptionType: v, DimensionKeyResourceType: p.resourceType, } - p.publishMetric(MetricNameHanderException, dimensions, cloudwatch.StandardUnitCount, 1.0, date) + p.publishMetric(MetricNameHanderException, dimensions, cwtypes.StandardUnitCount, 1.0, date) } -// PublishInvocationMetric publishes an invocation metric. func (p *Publisher) PublishInvocationMetric(date time.Time, action string) { dimensions := map[string]string{ - DimensionKeyAcionType: string(action), + DimensionKeyAcionType: action, DimensionKeyResourceType: p.resourceType, } - p.publishMetric(MetricNameHanderInvocationCount, dimensions, cloudwatch.StandardUnitCount, 1.0, date) + p.publishMetric(MetricNameHanderInvocationCount, dimensions, cwtypes.StandardUnitCount, 1.0, date) } -// PublishDurationMetric publishes an duration metric. -// -// A duration metric is the timing of something. func (p *Publisher) PublishDurationMetric(date time.Time, action string, secs float64) { dimensions := map[string]string{ - DimensionKeyAcionType: string(action), + DimensionKeyAcionType: action, DimensionKeyResourceType: p.resourceType, } - p.publishMetric(MetricNameHanderDuration, dimensions, cloudwatch.StandardUnitMilliseconds, secs, date) + p.publishMetric(MetricNameHanderDuration, dimensions, cwtypes.StandardUnitMilliseconds, secs, date) } -func (p *Publisher) publishMetric(metricName string, data map[string]string, unit string, value float64, date time.Time) { - - var d []*cloudwatch.Dimension - +func (p *Publisher) publishMetric(metricName string, data map[string]string, unit cwtypes.StandardUnit, value float64, date time.Time) { + var d []cwtypes.Dimension for k, v := range data { - dim := &cloudwatch.Dimension{ + d = append(d, cwtypes.Dimension{ Name: aws.String(k), Value: aws.String(v), - } - d = append(d, dim) + }) } - md := []*cloudwatch.MetricDatum{ + + md := []cwtypes.MetricDatum{ { MetricName: aws.String(metricName), - Unit: aws.String(unit), + Unit: unit, Value: aws.Float64(value), Dimensions: d, - Timestamp: &date}, + Timestamp: &date, + }, } - pi := cloudwatch.PutMetricDataInput{ + + _, err := p.client.PutMetricData(context.Background(), &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) - } } -// ResourceTypeName returns a type name by removing (::) and replaing with (/) -// -// Example -// -// r := metrics.ResourceTypeName("AWS::Service::Resource") -// -// // Will return "AWS/Service/Resource" +// ResourceTypeName returns a type name by replacing (::) with (/). func ResourceTypeName(t string) string { return strings.ReplaceAll(t, "::", "/") - } diff --git a/cfn/metrics/publisher_test.go b/cfn/metrics/publisher_test.go index 5b1f6220..39f199bd 100644 --- a/cfn/metrics/publisher_test.go +++ b/cfn/metrics/publisher_test.go @@ -1,22 +1,21 @@ package metrics import ( + "context" "errors" "testing" "time" - "github.com/aws/aws-sdk-go/service/cloudwatch" - "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + cwtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" ) const succeed = "\u2713" const failed = "\u2717" -// Define a mock struct to be used in your unit tests of myFunc. type MockCloudWatchClient struct { - cloudwatchiface.CloudWatchAPI MetricName string - Unit string + Unit cwtypes.StandardUnit Value float64 Dim map[string]string } @@ -25,40 +24,31 @@ func NewMockCloudWatchClient() *MockCloudWatchClient { return &MockCloudWatchClient{} } -func (m *MockCloudWatchClient) PutMetricData(in *cloudwatch.PutMetricDataInput) (*cloudwatch.PutMetricDataOutput, error) { - - // copy dimension in to a map for searching - +func (m *MockCloudWatchClient) PutMetricData(ctx context.Context, in *cloudwatch.PutMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) { d := make(map[string]string) - for _, v := range in.MetricData[0].Dimensions { d[*v.Name] = *v.Value } - m.MetricName = *in.MetricData[0].MetricName - m.Unit = *in.MetricData[0].Unit + m.Unit = in.MetricData[0].Unit m.Value = *in.MetricData[0].Value m.Dim = d - return nil, nil } -// Define a mock struct to be used in your unit tests of myFunc. -type MockCloudWatchClientError struct { - cloudwatchiface.CloudWatchAPI -} +type MockCloudWatchClientError struct{} func NewMockCloudWatchClientError() *MockCloudWatchClientError { return &MockCloudWatchClientError{} } -func (m *MockCloudWatchClientError) PutMetricData(in *cloudwatch.PutMetricDataInput) (*cloudwatch.PutMetricDataOutput, error) { - +func (m *MockCloudWatchClientError) PutMetricData(ctx context.Context, in *cloudwatch.PutMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) { return nil, errors.New("Error") } + func TestPublisher_PublishExceptionMetric(t *testing.T) { type fields struct { - Client cloudwatchiface.CloudWatchAPI + Client CloudWatchClient resName string } type args struct { @@ -67,242 +57,79 @@ func TestPublisher_PublishExceptionMetric(t *testing.T) { e error } tests := []struct { - name string - fields fields - args args - MetricName string - wantErr bool - wantAction string - wantDimensionKeyExceptionType string - wantDimensionKeyResourceType string - wantMetricName string - wantUnit string - wantValue float64 + name string + fields fields + args args + wantErr bool + wantAction string + wantExc string + wantRes string + wantMetric string + wantUnit cwtypes.StandardUnit + wantValue float64 }{ - {"testPublisherPublishExceptionMetric", fields{NewMockCloudWatchClient(), "foo::bar::test"}, 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"}, 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"}, 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"}, 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}, + {"exception", fields{NewMockCloudWatchClient(), "foo::bar::test"}, args{time.Now(), "CREATE", errors.New("failed to create\nresource")}, false, "CREATE", "failed to create resource", "foo/bar/test", "HandlerException", cwtypes.StandardUnitCount, 1.0}, + {"exception error", fields{NewMockCloudWatchClientError(), "foo::bar::test"}, args{time.Now(), "CREATE", errors.New("failed")}, true, "", "", "", "", "", 0}, } - for i, tt := range tests { + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := New(tt.fields.Client, tt.fields.resName) - t.Logf("\tTest: %d\tWhen checking %q for success", i, tt.name) - { - p.PublishExceptionMetric(tt.args.date, tt.args.action, tt.args.e) - t.Logf("\t%s\tShould be able to make the PublishExceptionMetric call.", succeed) - if !tt.wantErr { - e := tt.fields.Client.(*MockCloudWatchClient) - - if e.Dim[DimensionKeyAcionType] == tt.wantAction { - t.Logf("\t%s\tAction should be (%v).", succeed, tt.wantAction) - } else { - t.Errorf("\t%s\tAction should be (%v). : %v", failed, tt.wantAction, e.Dim[DimensionKeyAcionType]) - } - - if e.Dim[DimensionKeyExceptionType] == tt.wantDimensionKeyExceptionType { - t.Logf("\t%s\tDimensionKeyExceptionType should be (%v).", succeed, tt.wantDimensionKeyExceptionType) - } else { - t.Errorf("\t%s\tDimensionKeyExceptionType should be (%v). : %v", failed, tt.wantDimensionKeyExceptionType, e.Dim[DimensionKeyExceptionType]) - } - - if e.Dim[DimensionKeyResourceType] == tt.wantDimensionKeyResourceType { - t.Logf("\t%s\t DimensionKeyResourceType should be (%v).", succeed, tt.wantDimensionKeyResourceType) - } else { - t.Errorf("\t%s\tDimensionKeyResourceType should be (%v). : %v", failed, tt.wantDimensionKeyResourceType, e.Dim[DimensionKeyResourceType]) - } - - if e.MetricName == tt.wantMetricName { - t.Logf("\t%s\t MetricName should be (%v).", succeed, tt.wantMetricName) - } else { - t.Errorf("\t%s\tMetricName should be (%v). : %v", failed, tt.wantMetricName, e.MetricName) - } - - if e.Unit == tt.wantUnit { - t.Logf("\t%s\t Unit should be (%v).", succeed, tt.wantUnit) - } else { - t.Errorf("\t%s\tUnit should be (%v). : %v", failed, tt.wantUnit, e.Unit) - } - - if e.Value == tt.wantValue { - t.Logf("\t%s\t Unit should be (%v).", succeed, tt.wantValue) - } else { - t.Errorf("\t%s\tUnit should be (%v). : %v", failed, tt.wantValue, e.Value) - } + p.PublishExceptionMetric(tt.args.date, tt.args.action, tt.args.e) + if !tt.wantErr { + e := tt.fields.Client.(*MockCloudWatchClient) + if e.Dim[DimensionKeyAcionType] != tt.wantAction { + t.Errorf("Action: got %v, want %v", e.Dim[DimensionKeyAcionType], tt.wantAction) + } + if e.Dim[DimensionKeyExceptionType] != tt.wantExc { + t.Errorf("ExceptionType: got %v, want %v", e.Dim[DimensionKeyExceptionType], tt.wantExc) + } + if e.Dim[DimensionKeyResourceType] != tt.wantRes { + t.Errorf("ResourceType: got %v, want %v", e.Dim[DimensionKeyResourceType], tt.wantRes) + } + if e.MetricName != tt.wantMetric { + t.Errorf("MetricName: got %v, want %v", e.MetricName, tt.wantMetric) + } + if e.Unit != tt.wantUnit { + t.Errorf("Unit: got %v, want %v", e.Unit, tt.wantUnit) + } + if e.Value != tt.wantValue { + t.Errorf("Value: got %v, want %v", e.Value, tt.wantValue) } } - }) } } func TestPublisher_PublishInvocationMetric(t *testing.T) { - type fields struct { - Client cloudwatchiface.CloudWatchAPI - resName string + p := New(NewMockCloudWatchClient(), "foo::bar::test") + p.PublishInvocationMetric(time.Now(), "CREATE") + e := p.client.(*MockCloudWatchClient) + if e.MetricName != MetricNameHanderInvocationCount { + t.Errorf("MetricName: got %v, want %v", e.MetricName, MetricNameHanderInvocationCount) } - type args struct { - date time.Time - action string - } - tests := []struct { - name string - fields fields - args args - MetricName string - wantErr bool - wantAction string - wantDimensionKeyResourceType string - wantMetricName string - wantUnit string - wantValue float64 - }{ - {"testPublishInvocationMetric", fields{NewMockCloudWatchClient(), "foo::bar::test"}, args{time.Now(), "CREATE"}, "HandlerInvocationCount", false, "CREATE", "foo/bar/test", "HandlerInvocationCount", cloudwatch.StandardUnitCount, 1.0}, - {"testPublishInvocationMetricWantError", fields{NewMockCloudWatchClientError(), "foo::bar::test"}, args{time.Now(), "CREATE"}, "HandlerException", true, "CREATE", "foo/bar/test", "HandlerException", cloudwatch.StandardUnitCount, 1.0}, - {"testPublishInvocationMetric", fields{NewMockCloudWatchClient(), "foo::bar::test"}, args{time.Now(), "UPDATE"}, "HandlerInvocationCount", false, "UPDATE", "foo/bar/test", "HandlerInvocationCount", cloudwatch.StandardUnitCount, 1.0}, - {"testPublishInvocationMetricError", fields{NewMockCloudWatchClientError(), "foo::bar::test"}, args{time.Now(), "UPDATE"}, "HandlerException", true, "UPDATE", "foo/bar/test", "HandlerInvocationCount", cloudwatch.StandardUnitCount, 1.0}, + if e.Unit != cwtypes.StandardUnitCount { + t.Errorf("Unit: got %v, want %v", e.Unit, cwtypes.StandardUnitCount) } - for i, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := New(tt.fields.Client, tt.fields.resName) - t.Logf("\tTest: %d\tWhen checking %q for success", i, tt.name) - { - p.PublishInvocationMetric(tt.args.date, tt.args.action) - t.Logf("\t%s\tShould be able to make the PublishInvocationMetric call.", succeed) - if !tt.wantErr { - e := tt.fields.Client.(*MockCloudWatchClient) - - if e.Dim[DimensionKeyAcionType] == tt.wantAction { - t.Logf("\t%s\tAction should be (%v).", succeed, tt.wantAction) - } else { - t.Errorf("\t%s\tAction should be (%v). : %v", failed, tt.wantAction, e.Dim[DimensionKeyAcionType]) - } - - if e.Dim[DimensionKeyResourceType] == tt.wantDimensionKeyResourceType { - t.Logf("\t%s\t DimensionKeyResourceType should be (%v).", succeed, tt.wantDimensionKeyResourceType) - } else { - t.Errorf("\t%s\tDimensionKeyResourceType should be (%v). : %v", failed, tt.wantDimensionKeyResourceType, e.Dim[DimensionKeyResourceType]) - } - - if e.MetricName == tt.wantMetricName { - t.Logf("\t%s\t MetricName should be (%v).", succeed, tt.wantMetricName) - } else { - t.Errorf("\t%s\tMetricName should be (%v). : %v", failed, tt.wantMetricName, e.MetricName) - } - - if e.Unit == tt.wantUnit { - t.Logf("\t%s\t Unit should be (%v).", succeed, tt.wantUnit) - } else { - t.Errorf("\t%s\tUnit should be (%v). : %v", failed, tt.wantUnit, e.Unit) - } - - if e.Value == tt.wantValue { - t.Logf("\t%s\t Unit should be (%v).", succeed, tt.wantValue) - } else { - t.Errorf("\t%s\tUnit should be (%v). : %v", failed, tt.wantValue, e.Value) - } - } - } - - }) - } - } func TestPublisher_PublishDurationMetric(t *testing.T) { - type fields struct { - Client cloudwatchiface.CloudWatchAPI - resName string - } - type args struct { - date time.Time - action string - sec float64 + p := New(NewMockCloudWatchClient(), "foo::bar::test") + p.PublishDurationMetric(time.Now(), "CREATE", 15.0) + e := p.client.(*MockCloudWatchClient) + if e.MetricName != MetricNameHanderDuration { + t.Errorf("MetricName: got %v, want %v", e.MetricName, MetricNameHanderDuration) } - tests := []struct { - name string - fields fields - args args - MetricName string - wantErr bool - wantAction string - wantDimensionKeyResourceType string - wantMetricName string - wantUnit string - wantValue float64 - }{ - {"testPublishInvocationMetric", fields{NewMockCloudWatchClient(), "foo::bar::test"}, args{time.Now(), "CREATE", 15.0}, "HandlerInvocationDuration", false, "CREATE", "foo/bar/test", "HandlerInvocationDuration", cloudwatch.StandardUnitMilliseconds, 15}, - {"testPublishInvocationMetricWantError", fields{NewMockCloudWatchClientError(), "foo::bar::test"}, args{time.Now(), "CREATE", 15.0}, "HandlerInvocationDuration", true, "CREATE", "foo/bar/test", "HandlerInvocationDuration", cloudwatch.StandardUnitMilliseconds, 15}, - {"testPublishInvocationMetric", fields{NewMockCloudWatchClient(), "foo::bar::test"}, args{time.Now(), "UPDATE", 15.0}, "HandlerInvocationDuration", false, "UPDATE", "foo/bar/test", "HandlerInvocationDuration", cloudwatch.StandardUnitMilliseconds, 15}, - {"testPublishInvocationMetricError", fields{NewMockCloudWatchClientError(), "foo::bar::test"}, args{time.Now(), "UPDATE", 15.0}, "HandlerInvocationDuration", true, "UPDATE", "foo/bar/test", "HandlerInvocationDuration", cloudwatch.StandardUnitMilliseconds, 15}, + if e.Unit != cwtypes.StandardUnitMilliseconds { + t.Errorf("Unit: got %v, want %v", e.Unit, cwtypes.StandardUnitMilliseconds) } - for i, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := New(tt.fields.Client, tt.fields.resName) - t.Logf("\tTest: %d\tWhen checking %q for success", i, tt.name) - { - p.PublishDurationMetric(tt.args.date, tt.args.action, tt.args.sec) - t.Logf("\t%s\tShould be able to make the PublishDurationMetric call.", succeed) - if !tt.wantErr { - e := tt.fields.Client.(*MockCloudWatchClient) - - if e.Dim[DimensionKeyAcionType] == tt.wantAction { - t.Logf("\t%s\tAction should be (%v).", succeed, tt.wantAction) - } else { - t.Errorf("\t%s\tAction should be (%v). : %v", failed, tt.wantAction, e.Dim[DimensionKeyAcionType]) - } - - if e.Dim[DimensionKeyResourceType] == tt.wantDimensionKeyResourceType { - t.Logf("\t%s\t DimensionKeyResourceType should be (%v).", succeed, tt.wantDimensionKeyResourceType) - } else { - t.Errorf("\t%s\tDimensionKeyResourceType should be (%v). : %v", failed, tt.wantDimensionKeyResourceType, e.Dim[DimensionKeyResourceType]) - } - - if e.MetricName == tt.wantMetricName { - t.Logf("\t%s\t MetricName should be (%v).", succeed, tt.wantMetricName) - } else { - t.Errorf("\t%s\tMetricName should be (%v). : %v", failed, tt.wantMetricName, e.MetricName) - } - - if e.Unit == tt.wantUnit { - t.Logf("\t%s\t Unit should be (%v).", succeed, tt.wantUnit) - } else { - t.Errorf("\t%s\tUnit should be (%v). : %v", failed, tt.wantUnit, e.Unit) - } - - if e.Value == tt.wantValue { - t.Logf("\t%s\t Unit should be (%v).", succeed, tt.wantValue) - } else { - t.Errorf("\t%s\tUnit should be (%v). : %v", failed, tt.wantValue, e.Value) - } - } - } - - }) + if e.Value != 15.0 { + t.Errorf("Value: got %v, want %v", e.Value, 15.0) } - } func TestPublisher_ResourceTypeName(t *testing.T) { - type args struct { - t string - } - tests := []struct { - name string - args args - want string - }{ - {"test foo", args{"foo::bar::test"}, "foo/bar/test"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := ResourceTypeName(tt.args.t) - - if r != tt.want { - t.Errorf("Should be %v : got %v", tt.want, r) - return - } - - }) + r := ResourceTypeName("foo::bar::test") + if r != "foo/bar/test" { + t.Errorf("got %v, want foo/bar/test", r) } } diff --git a/cfn/response.go b/cfn/response.go index febe640b..4623a0c2 100644 --- a/cfn/response.go +++ b/cfn/response.go @@ -3,67 +3,32 @@ package cfn import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "github.com/aws/aws-sdk-go/service/cloudformation" + cftypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" ) // response represents a response to the // cloudformation service from a resource handler. -// The zero value is ready to use. type response struct { - // Message which can be shown to callers to indicate the nature of a - // progress transition or callback delay; for example a message - // indicating "propagating to edge" - Message string `json:"message,omitempty"` - - // 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:"status,omitempty"` - - // ResourceModel it The output resource instance populated by a READ/LIST for - // synchronous results and by CREATE/UPDATE/DELETE for final response - // validation/confirmation - ResourceModel interface{} `json:"resourceModel,omitempty"` - - // ErrorCode is used to report granular failures back to CloudFormation - ErrorCode string `json:"errorCode,omitempty"` - - // BearerToken is used to report progress back to CloudFormation and is - // 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"` - - // 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"` + Message string `json:"message,omitempty"` + OperationStatus handler.Status `json:"status,omitempty"` + ResourceModel interface{} `json:"resourceModel,omitempty"` + ErrorCode string `json:"errorCode,omitempty"` + BearerToken string `json:"bearerToken,omitempty"` + ResourceModels []interface{} `json:"resourceModels"` + NextToken string `json:"nextToken,omitempty"` + CallbackContext map[string]interface{} `json:"callbackContext,omitempty"` + CallbackDelaySeconds int64 `json:"callbackDelaySeconds,omitempty"` } -// newFailedResponse returns a response pre-filled with the supplied error func newFailedResponse(err error, bearerToken string) response { return response{ OperationStatus: handler.Failed, - ErrorCode: cloudformation.HandlerErrorCodeInternalFailure, + ErrorCode: string(cftypes.HandlerErrorCodeInternalFailure), Message: err.Error(), BearerToken: bearerToken, } } -// newResponse converts a progress event into a useable reponse -// for the CloudFormation Resource Provider service to understand. func newResponse(pevt *handler.ProgressEvent, bearerToken string) (response, error) { model, err := encoding.Stringify(pevt.ResourceModel) if err != nil { @@ -78,7 +43,6 @@ func newResponse(pevt *handler.ProgressEvent, bearerToken string) (response, err if err != nil { return response{}, err } - models[i] = m } } diff --git a/cfn/response_test.go b/cfn/response_test.go index fb296079..68a3b84f 100644 --- a/cfn/response_test.go +++ b/cfn/response_test.go @@ -1,15 +1,13 @@ package cfn import ( - "testing" - - "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/google/go-cmp/cmp" - "encoding/json" + "testing" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + cftypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/google/go-cmp/cmp" ) func TestResponseMarshalJSON(t *testing.T) { @@ -32,7 +30,7 @@ func TestResponseMarshalJSON(t *testing.T) { Name: encoding.NewString("Douglas"), Version: encoding.NewFloat(42.1), }, - ErrorCode: cloudformation.HandlerErrorCodeNotUpdatable, + ErrorCode: string(cftypes.HandlerErrorCodeNotUpdatable), BearerToken: "xyzzy", }, expected: `{"message":"foo","status":"FAILED","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy","resourceModels":null}`, @@ -62,16 +60,13 @@ func TestResponseMarshalJSON(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - actual, err := json.Marshal(tt.response) if err != nil { t.Errorf("Unexpected error marshaling response JSON: %s", err) } - if diff := cmp.Diff(string(actual), tt.expected); diff != "" { - t.Errorf(diff) + t.Errorf("%s", diff) } }) } - } diff --git a/cfn/scheduler/noop_scheduler.go b/cfn/scheduler/noop_scheduler.go deleted file mode 100644 index 43cc1cab..00000000 --- a/cfn/scheduler/noop_scheduler.go +++ /dev/null @@ -1,45 +0,0 @@ -package scheduler - -import ( - "log" - - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" - "github.com/aws/aws-sdk-go/service/cloudwatchevents/cloudwatcheventsiface" -) - -type noopCloudWatchClient struct { - cloudwatcheventsiface.CloudWatchEventsAPI - logger *log.Logger -} - -func newNoopCloudWatchClient() *noopCloudWatchClient { - return &noopCloudWatchClient{ - logger: logging.New("scheduler"), - } -} - -func (m *noopCloudWatchClient) PutRule(in *cloudwatchevents.PutRuleInput) (*cloudwatchevents.PutRuleOutput, error) { - m.logger.Printf("Rule name: %v", *in.Name) - // out implementation doesn't care about the response - return nil, nil -} - -func (m *noopCloudWatchClient) PutTargets(in *cloudwatchevents.PutTargetsInput) (*cloudwatchevents.PutTargetsOutput, error) { - m.logger.Printf("Target ID: %v", *in.Targets[0].Id) - // out implementation doesn't care about the response - return nil, nil - -} - -func (m *noopCloudWatchClient) DeleteRule(in *cloudwatchevents.DeleteRuleInput) (*cloudwatchevents.DeleteRuleOutput, error) { - m.logger.Printf("Rule name: %v", *in.Name) - // out implementation doesn't care about the response - return nil, nil -} - -func (m *noopCloudWatchClient) RemoveTargets(in *cloudwatchevents.RemoveTargetsInput) (*cloudwatchevents.RemoveTargetsOutput, error) { - m.logger.Printf("Target ID: %v", *in.Ids[0]) - // out implementation doesn't care about the response - return nil, nil -} diff --git a/cfn/scheduler/scheduler.go b/cfn/scheduler/scheduler.go index 75493f6e..e6c700c5 100644 --- a/cfn/scheduler/scheduler.go +++ b/cfn/scheduler/scheduler.go @@ -1,6 +1,3 @@ -//go:build scheduler -// +build scheduler - /* Package scheduler handles rescheduling resource provider handlers when required by in_progress events. @@ -17,9 +14,9 @@ import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" "github.com/aws/aws-lambda-go/lambdacontext" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" - "github.com/aws/aws-sdk-go/service/cloudwatchevents/cloudwatcheventsiface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchevents" + cwetypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchevents/types" "github.com/google/uuid" ) @@ -29,97 +26,87 @@ const ( ) const ( - // ServiceInternalError is used when there's a downstream error - // in the code. ServiceInternalError string = "ServiceInternal" ) +// CloudWatchEventsClient is the subset of the CloudWatch Events API used by this package. +type CloudWatchEventsClient interface { + PutRule(ctx context.Context, params *cloudwatchevents.PutRuleInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.PutRuleOutput, error) + PutTargets(ctx context.Context, params *cloudwatchevents.PutTargetsInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.PutTargetsOutput, error) + DeleteRule(ctx context.Context, params *cloudwatchevents.DeleteRuleInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.DeleteRuleOutput, error) + RemoveTargets(ctx context.Context, params *cloudwatchevents.RemoveTargetsInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.RemoveTargetsOutput, error) +} + // Result holds the confirmation of the rescheduled invocation. type Result struct { - // Denotes if the computation was done locally. ComputeLocal bool IDS ScheduleIDS } // ScheduleIDS is of the invocation type ScheduleIDS struct { - // The Cloudwatch target ID. - Target string - // The Cloudwatch handler ID. + Target string Handler string } -// Scheduler is the implementation of the rescheduler of an invoke -// -// Invokes will be rescheduled if a handler takes longer than 60 -// seconds. The invoke is rescheduled through CloudWatch Events -// via a CRON expression +// Scheduler reschedules handler invocations via CloudWatch Events. type Scheduler struct { logger *log.Logger - client cloudwatcheventsiface.CloudWatchEventsAPI + client CloudWatchEventsClient } -// New creates a CloudWatchScheduler and returns a pointer to the struct. -func New(client cloudwatcheventsiface.CloudWatchEventsAPI) *Scheduler { +// New creates a Scheduler. +func New(client CloudWatchEventsClient) *Scheduler { return &Scheduler{ logger: logging.New("scheduler"), client: client, } } -// Reschedule when a handler requests a sub-minute callback delay, and if the lambda -// invocation has enough runtime (with 20% buffer), we can reschedule from a thread wait -// otherwise we re-invoke through CloudWatchEvents which have a granularity of -// minutes. re-invoke through CloudWatchEvents no less than 1 minute from now. +// Reschedule handles re-invocation logic. func (s *Scheduler) Reschedule(lambdaCtx context.Context, secsFromNow int64, callbackRequest string, invocationIDS *ScheduleIDS) (*Result, error) { - lc, hasValue := lambdacontext.FromContext(lambdaCtx) - if !hasValue { return nil, cfnerr.New(ServiceInternalError, "Lambda Context has no value", errors.New("Lambda Context has no value")) } deadline, _ := lambdaCtx.Deadline() - secondsUnitDeadline := time.Until(deadline).Seconds() + secondsUntilDeadline := time.Until(deadline).Seconds() if secsFromNow <= 0 { - err := errors.New("Scheduled seconds must be greater than 0") - return nil, cfnerr.New(ServiceInternalError, "Scheduled seconds must be greater than 0", err) + return nil, cfnerr.New(ServiceInternalError, "Scheduled seconds must be greater than 0", errors.New("Scheduled seconds must be greater than 0")) } - if secsFromNow < 60 && secondsUnitDeadline > float64(secsFromNow)*1.2 { - - s.logger.Printf("Scheduling re-invoke locally after %v seconds, with Context %s", secsFromNow, string(callbackRequest)) - + if secsFromNow < 60 && secondsUntilDeadline > float64(secsFromNow)*1.2 { + s.logger.Printf("Scheduling re-invoke locally after %v seconds, with Context %s", secsFromNow, callbackRequest) time.Sleep(time.Duration(secsFromNow) * time.Second) - return &Result{ComputeLocal: true, IDS: *invocationIDS}, nil } - // re-invoke through CloudWatchEvents no less than 1 minute from now. if secsFromNow < 60 { secsFromNow = 60 } + ctx := context.Background() cr := GenerateOneTimeCronExpression(secsFromNow, time.Now()) - s.logger.Printf("Scheduling re-invoke at %s \n", cr) - _, rerr := s.client.PutRule(&cloudwatchevents.PutRuleInput{ + s.logger.Printf("Scheduling re-invoke at %s\n", cr) + _, rerr := s.client.PutRule(ctx, &cloudwatchevents.PutRuleInput{ Name: aws.String(invocationIDS.Handler), ScheduleExpression: aws.String(cr), - State: aws.String(cloudwatchevents.RuleStateEnabled), + State: cwetypes.RuleStateEnabled, }) - if rerr != nil { return nil, cfnerr.New(ServiceInternalError, "Schedule error", rerr) } - _, perr := s.client.PutTargets(&cloudwatchevents.PutTargetsInput{ + + _, perr := s.client.PutTargets(ctx, &cloudwatchevents.PutTargetsInput{ Rule: aws.String(invocationIDS.Handler), - Targets: []*cloudwatchevents.Target{ - &cloudwatchevents.Target{ + Targets: []cwetypes.Target{ + { Arn: aws.String(lc.InvokedFunctionArn), Id: aws.String(invocationIDS.Target), - Input: aws.String(string(callbackRequest)), + Input: aws.String(callbackRequest), }, }, }) @@ -130,20 +117,19 @@ func (s *Scheduler) Reschedule(lambdaCtx context.Context, secsFromNow int64, cal return &Result{ComputeLocal: false, IDS: *invocationIDS}, nil } -// CleanupEvents is used to clean up Cloudwatch Events. -// After a re-invocation, the CWE rule which generated the reinvocation should be scrubbed. +// CleanupEvents removes CloudWatch Events rule and target after re-invocation. func (s *Scheduler) CleanupEvents(ruleName string, targetID string) error { - if len(ruleName) == 0 { return cfnerr.New(ServiceInternalError, "Unable to complete request", errors.New("ruleName is required")) } if len(targetID) == 0 { return cfnerr.New(ServiceInternalError, "Unable to complete request", errors.New("targetID is required")) } - _, err := s.client.RemoveTargets(&cloudwatchevents.RemoveTargetsInput{ - Ids: []*string{ - aws.String(targetID), - }, + + ctx := context.Background() + + _, err := s.client.RemoveTargets(ctx, &cloudwatchevents.RemoveTargetsInput{ + Ids: []string{targetID}, Rule: aws.String(ruleName), }) if err != nil { @@ -153,7 +139,7 @@ func (s *Scheduler) CleanupEvents(ruleName string, targetID string) error { } s.logger.Printf("CloudWatchEvents Target (targetId=%s) removed", targetID) - _, rerr := s.client.DeleteRule(&cloudwatchevents.DeleteRuleInput{ + _, rerr := s.client.DeleteRule(ctx, &cloudwatchevents.DeleteRuleInput{ Name: aws.String(ruleName), }) if rerr != nil { @@ -166,31 +152,21 @@ func (s *Scheduler) CleanupEvents(ruleName string, targetID string) error { return nil } -// GenerateOneTimeCronExpression a cron(..) expression for a single instance -// at Now+minutesFromNow -// -// Example -// -// // Will generate a cron string of: "1 0 0 0 0" -// scheduler.GenerateOneTimeCronExpression(60, time.Now()) +// GenerateOneTimeCronExpression generates a cron expression for a single invocation. func GenerateOneTimeCronExpression(secFromNow int64, t time.Time) string { a := t.Add(time.Second * time.Duration(secFromNow)) return fmt.Sprintf("cron(%02d %02d %02d %02d ? %d)", a.Minute(), a.Hour(), a.Day(), a.Month(), a.Year()) } -// GenerateCloudWatchIDS creates the targetID and handlerID for invocation +// GenerateCloudWatchIDS creates the targetID and handlerID for invocation. func GenerateCloudWatchIDS() (*ScheduleIDS, error) { - uuid, err := uuid.NewUUID() - + u, err := uuid.NewUUID() if err != nil { return nil, cfnerr.New(ServiceInternalError, "uuid error", err) } - handlerID := fmt.Sprintf(HandlerPrepend, uuid) - targetID := fmt.Sprintf(TargentPrepend, uuid) - return &ScheduleIDS{ - Target: targetID, - Handler: handlerID, + Target: fmt.Sprintf(TargentPrepend, u), + Handler: fmt.Sprintf(HandlerPrepend, u), }, nil } diff --git a/cfn/scheduler/scheduler_notag.go b/cfn/scheduler/scheduler_notag.go deleted file mode 100644 index cd0b909e..00000000 --- a/cfn/scheduler/scheduler_notag.go +++ /dev/null @@ -1,196 +0,0 @@ -//go:build !scheduler -// +build !scheduler - -/* -Package scheduler handles rescheduling resource provider handlers -when required by in_progress events. -*/ -package scheduler - -import ( - "context" - "errors" - "fmt" - "log" - "time" - - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/cfnerr" - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" - "github.com/aws/aws-lambda-go/lambdacontext" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" - "github.com/aws/aws-sdk-go/service/cloudwatchevents/cloudwatcheventsiface" - "github.com/google/uuid" -) - -const ( - HandlerPrepend string = "reinvoke-handler-%s" - TargentPrepend string = "reinvoke-target-%s" -) - -const ( - // ServiceInternalError is used when there's a downstream error - // in the code. - ServiceInternalError string = "ServiceInternal" -) - -// Result holds the confirmation of the rescheduled invocation. -type Result struct { - // Denotes if the computation was done locally. - ComputeLocal bool - IDS ScheduleIDS -} - -// ScheduleIDS is of the invocation -type ScheduleIDS struct { - // The Cloudwatch target ID. - Target string - // The Cloudwatch handler ID. - Handler string -} - -// Scheduler is the implementation of the rescheduler of an invoke -// -// Invokes will be rescheduled if a handler takes longer than 60 -// seconds. The invoke is rescheduled through CloudWatch Events -// via a CRON expression -type Scheduler struct { - client cloudwatcheventsiface.CloudWatchEventsAPI - logger *log.Logger -} - -// New creates a CloudWatchScheduler and returns a pointer to the struct. -func New(client cloudwatcheventsiface.CloudWatchEventsAPI) *Scheduler { - return &Scheduler{ - logger: logging.New("scheduler"), - client: newNoopCloudWatchClient(), - } -} - -// Reschedule when a handler requests a sub-minute callback delay, and if the lambda -// invocation has enough runtime (with 20% buffer), we can reschedule from a thread wait -// otherwise we re-invoke through CloudWatchEvents which have a granularity of -// minutes. re-invoke through CloudWatchEvents no less than 1 minute from now. -func (s *Scheduler) Reschedule(lambdaCtx context.Context, secsFromNow int64, callbackRequest string, invocationIDS *ScheduleIDS) (*Result, error) { - - lc, hasValue := lambdacontext.FromContext(lambdaCtx) - - if !hasValue { - return nil, cfnerr.New(ServiceInternalError, "Lambda Context has no value", errors.New("Lambda Context has no value")) - } - - deadline, _ := lambdaCtx.Deadline() - secondsUnitDeadline := time.Until(deadline).Seconds() - - if secsFromNow <= 0 { - err := errors.New("Scheduled seconds must be greater than 0") - return nil, cfnerr.New(ServiceInternalError, "Scheduled seconds must be greater than 0", err) - } - - if secsFromNow < 60 && secondsUnitDeadline > float64(secsFromNow)*1.2 { - - s.logger.Printf("Scheduling re-invoke locally after %v seconds, with Context %s", secsFromNow, string(callbackRequest)) - - time.Sleep(time.Duration(secsFromNow) * time.Second) - - return &Result{ComputeLocal: true, IDS: *invocationIDS}, nil - } - - // re-invoke through CloudWatchEvents no less than 1 minute from now. - if secsFromNow < 60 { - secsFromNow = 60 - } - - cr := GenerateOneTimeCronExpression(secsFromNow, time.Now()) - s.logger.Printf("Scheduling re-invoke at %s \n", cr) - _, rerr := s.client.PutRule(&cloudwatchevents.PutRuleInput{ - - Name: aws.String(invocationIDS.Handler), - ScheduleExpression: aws.String(cr), - State: aws.String(cloudwatchevents.RuleStateEnabled), - }) - - if rerr != nil { - return nil, cfnerr.New(ServiceInternalError, "Schedule error", rerr) - } - _, perr := s.client.PutTargets(&cloudwatchevents.PutTargetsInput{ - Rule: aws.String(invocationIDS.Handler), - Targets: []*cloudwatchevents.Target{ - { - Arn: aws.String(lc.InvokedFunctionArn), - Id: aws.String(invocationIDS.Target), - Input: aws.String(string(callbackRequest)), - }, - }, - }) - if perr != nil { - return nil, cfnerr.New(ServiceInternalError, "Schedule error", perr) - } - - return &Result{ComputeLocal: false, IDS: *invocationIDS}, nil -} - -// CleanupEvents is used to clean up Cloudwatch Events. -// After a re-invocation, the CWE rule which generated the reinvocation should be scrubbed. -func (s *Scheduler) CleanupEvents(ruleName string, targetID string) error { - - if len(ruleName) == 0 { - return cfnerr.New(ServiceInternalError, "Unable to complete request", errors.New("ruleName is required")) - } - if len(targetID) == 0 { - return cfnerr.New(ServiceInternalError, "Unable to complete request", errors.New("targetID is required")) - } - _, err := s.client.RemoveTargets(&cloudwatchevents.RemoveTargetsInput{ - Ids: []*string{ - aws.String(targetID), - }, - Rule: aws.String(ruleName), - }) - if err != nil { - es := fmt.Sprintf("Error cleaning CloudWatchEvents Target (targetId=%s)", targetID) - s.logger.Println(es) - return cfnerr.New(ServiceInternalError, es, err) - } - s.logger.Printf("CloudWatchEvents Target (targetId=%s) removed", targetID) - - _, rerr := s.client.DeleteRule(&cloudwatchevents.DeleteRuleInput{ - Name: aws.String(ruleName), - }) - if rerr != nil { - es := fmt.Sprintf("Error cleaning CloudWatchEvents (ruleName=%s)", ruleName) - s.logger.Println(es) - return cfnerr.New(ServiceInternalError, es, rerr) - } - s.logger.Printf("CloudWatchEvents Rule (ruleName=%s) removed", ruleName) - - return nil -} - -// GenerateOneTimeCronExpression a cron(..) expression for a single instance -// at Now+minutesFromNow -// -// Example -// -// // Will generate a cron string of: "1 0 0 0 0" -// scheduler.GenerateOneTimeCronExpression(60, time.Now()) -func GenerateOneTimeCronExpression(secFromNow int64, t time.Time) string { - a := t.Add(time.Second * time.Duration(secFromNow)) - return fmt.Sprintf("cron(%02d %02d %02d %02d ? %d)", a.Minute(), a.Hour(), a.Day(), a.Month(), a.Year()) -} - -// GenerateCloudWatchIDS creates the targetID and handlerID for invocation -func GenerateCloudWatchIDS() (*ScheduleIDS, error) { - uuid, err := uuid.NewUUID() - - if err != nil { - return nil, cfnerr.New(ServiceInternalError, "uuid error", err) - } - - handlerID := fmt.Sprintf(HandlerPrepend, uuid) - targetID := fmt.Sprintf(TargentPrepend, uuid) - - return &ScheduleIDS{ - Target: targetID, - Handler: handlerID, - }, nil -} diff --git a/cfn/scheduler/scheduler_test.go b/cfn/scheduler/scheduler_test.go index 7234c1bb..4027077a 100644 --- a/cfn/scheduler/scheduler_test.go +++ b/cfn/scheduler/scheduler_test.go @@ -8,8 +8,7 @@ import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/logging" "github.com/aws/aws-lambda-go/lambdacontext" - "github.com/aws/aws-sdk-go/service/cloudwatchevents" - "github.com/aws/aws-sdk-go/service/cloudwatchevents/cloudwatcheventsiface" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchevents" ) const ( @@ -20,9 +19,7 @@ const ( Arn string = "arn:aws:lambda:us-east-2:123456789:function:myproject" ) -// MockedEvents mocks the call to AWS CloudWatch Events type MockedEvents struct { - cloudwatcheventsiface.CloudWatchEventsAPI RuleName string TargetName string } @@ -31,204 +28,123 @@ func NewMockEvents() *MockedEvents { return &MockedEvents{} } -func (m *MockedEvents) PutRule(in *cloudwatchevents.PutRuleInput) (*cloudwatchevents.PutRuleOutput, error) { +func (m *MockedEvents) PutRule(ctx context.Context, in *cloudwatchevents.PutRuleInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.PutRuleOutput, error) { m.RuleName = *in.Name return nil, nil } -func (m *MockedEvents) PutTargets(in *cloudwatchevents.PutTargetsInput) (*cloudwatchevents.PutTargetsOutput, error) { +func (m *MockedEvents) PutTargets(ctx context.Context, in *cloudwatchevents.PutTargetsInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.PutTargetsOutput, error) { m.TargetName = *in.Targets[0].Id return nil, nil - } -func (m *MockedEvents) DeleteRule(in *cloudwatchevents.DeleteRuleInput) (*cloudwatchevents.DeleteRuleOutput, error) { +func (m *MockedEvents) DeleteRule(ctx context.Context, in *cloudwatchevents.DeleteRuleInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.DeleteRuleOutput, error) { m.RuleName = *in.Name return nil, nil } -func (m *MockedEvents) RemoveTargets(in *cloudwatchevents.RemoveTargetsInput) (*cloudwatchevents.RemoveTargetsOutput, error) { - m.TargetName = *in.Ids[0] +func (m *MockedEvents) RemoveTargets(ctx context.Context, in *cloudwatchevents.RemoveTargetsInput, optFns ...func(*cloudwatchevents.Options)) (*cloudwatchevents.RemoveTargetsOutput, error) { + m.TargetName = in.Ids[0] return nil, nil } func TestGenerateOneTimeCronExpression(t *testing.T) { - type args struct { - minutesFromNow int64 - t time.Time - } tests := []struct { name string - args args + secs int64 + t time.Time want string }{ - {"TestCreateOneTimeCronExpression", args{0, time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)}, "cron(34 20 17 11 ? 2009)"}, - {"TestCreateOneTimeCronExpression", args{0, time.Date(2001, 5, 25, 11, 04, 14, 651387237, time.UTC)}, "cron(04 11 25 05 ? 2001)"}, - {"TestCreateOneTimeCronExpression", args{0, time.Date(2006, 7, 17, 7, 18, 23, 651387237, time.UTC)}, "cron(18 07 17 07 ? 2006)"}, - {"TestCreateOneTimeCronExpression", args{0, time.Date(1999, 2, 07, 21, 28, 45, 651387237, time.UTC)}, "cron(28 21 07 02 ? 1999)"}, + {"t1", 0, time.Date(2009, 11, 17, 20, 34, 58, 0, time.UTC), "cron(34 20 17 11 ? 2009)"}, + {"t2", 0, time.Date(2001, 5, 25, 11, 04, 14, 0, time.UTC), "cron(04 11 25 05 ? 2001)"}, + {"t3", 0, time.Date(2006, 7, 17, 7, 18, 23, 0, time.UTC), "cron(18 07 17 07 ? 2006)"}, + {"t4", 0, time.Date(1999, 2, 07, 21, 28, 45, 0, time.UTC), "cron(28 21 07 02 ? 1999)"}, } - for i, tt := range tests { + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Logf("\tTest: %d\tWhen checking %q for match status %v", i, tt.name, tt.want) - { - got := GenerateOneTimeCronExpression(tt.args.minutesFromNow, tt.args.t) - - if got == tt.want { - t.Logf("\t%s\tOneTimeCronExpression match should be (%v).", Succeed, tt.want) - } else { - t.Errorf("\t%s\tOneTimeCronExpression match should be (%v). : %v", Failed, tt.want, got) - } + got := GenerateOneTimeCronExpression(tt.secs, tt.t) + if got != tt.want { + t.Errorf("GenerateOneTimeCronExpression() = %v, want %v", got, tt.want) } }) } } -func TestCloudWatchSchedulerRescheduleAfterMinutes(t *testing.T) { - - var cb = `{ string: "Foo"}` - ct1, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*time.Duration(1000))) +func TestCloudWatchSchedulerReschedule(t *testing.T) { + cb := `{ string: "Foo"}` + ct1, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*1000)) defer cancel() - ct2, cancel2 := context.WithDeadline(context.Background(), time.Now().Add(time.Second*time.Duration(16))) + ct2, cancel2 := context.WithDeadline(context.Background(), time.Now().Add(time.Second*16)) defer cancel2() - type fields struct { - Client cloudwatcheventsiface.CloudWatchEventsAPI - } - type args struct { - ctx context.Context - secFromNow int64 - callbackContext string - } tests := []struct { name string - fields fields - args args + ctx context.Context + secFromNow int64 wantErr bool computeLocal bool }{ - {"TestCloudWatchScheduler56SecsComputeLocal", fields{NewMockEvents()}, args{lambdacontext.NewContext(ct1, &lambdacontext.LambdaContext{InvokedFunctionArn: Arn}), 15, cb}, false, true}, - {"TestCloudWatchScheduler56SecsComputeNotLocal", fields{NewMockEvents()}, args{lambdacontext.NewContext(ct2, &lambdacontext.LambdaContext{InvokedFunctionArn: Arn}), 15, cb}, false, false}, - {"TestCloudWatchSchedulerLessThen0", fields{NewMockEvents()}, args{lambdacontext.NewContext(ct1, &lambdacontext.LambdaContext{InvokedFunctionArn: Arn}), -87, cb}, true, false}, + {"local compute", lambdacontext.NewContext(ct1, &lambdacontext.LambdaContext{InvokedFunctionArn: Arn}), 15, false, true}, + {"remote compute", lambdacontext.NewContext(ct2, &lambdacontext.LambdaContext{InvokedFunctionArn: Arn}), 15, false, false}, + {"negative secs", lambdacontext.NewContext(ct1, &lambdacontext.LambdaContext{InvokedFunctionArn: Arn}), -87, true, false}, } - for i, tt := range tests { + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Logf("\tTest: %d\tWhen checking %q for error status %v", i, tt.name, tt.wantErr) - { - c := &Scheduler{ - client: tt.fields.Client, - logger: logging.New("scheduler: "), - } - - ids, _ := GenerateCloudWatchIDS() - cp, err := c.Reschedule(tt.args.ctx, tt.args.secFromNow, tt.args.callbackContext, ids) - if err != nil && - tt.wantErr { - - t.Logf("\t%s\tShould be able to make the RescheduleAfterMinutes call.", Succeed) - return - } - - if cp.ComputeLocal == tt.computeLocal { - t.Logf("\t%s\tCompute Local should be (%v).", Succeed, tt.computeLocal) - } else { - t.Errorf("\t%s\tCompute Local should be (%v). : Value:%v", Failed, tt.computeLocal, cp.ComputeLocal) - return - } + c := &Scheduler{ + client: NewMockEvents(), + logger: logging.New("scheduler: "), + } + ids, _ := GenerateCloudWatchIDS() + result, err := c.Reschedule(tt.ctx, tt.secFromNow, cb, ids) + if (err != nil) != tt.wantErr { + t.Errorf("Reschedule() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil && result.ComputeLocal != tt.computeLocal { + t.Errorf("ComputeLocal = %v, want %v", result.ComputeLocal, tt.computeLocal) } }) } - } -func TestCloudWatchSchedulerCleanupCloudWatchEvents(t *testing.T) { - type fields struct { - Client cloudwatcheventsiface.CloudWatchEventsAPI - } - type args struct { - cloudWatchEventsRuleName string - cloudWatchEventsTargetID string - } +func TestCloudWatchSchedulerCleanupEvents(t *testing.T) { tests := []struct { - name string - fields fields - args args - wantErr bool + name string + rule string + target string + wantErr bool }{ - {"TestCloudWatchRemove", fields{NewMockEvents()}, args{"reinvoke-handler-c51d7ba5-8eed-4226-99a6-6743f1169688", "reinvoke-target-c51d7ba5-8eed-4226-99a6-6743f1169688"}, false}, - {"TestCloudWatchRemoveBlankCloudWatchEventsRuleName", fields{NewMockEvents()}, args{"", "reinvoke-target-c51d7ba5-8eed-4226-99a6-6743f1169688"}, true}, - {"TestCloudWatchRemoveBlankcloudWatchEventsTargetID", fields{NewMockEvents()}, args{"reinvoke-handler-c51d7ba5-8eed-4226-99a6-6743f1169688", ""}, true}, + {"valid", "reinvoke-handler-c51d7ba5-8eed-4226-99a6-6743f1169688", "reinvoke-target-c51d7ba5-8eed-4226-99a6-6743f1169688", false}, + {"blank rule", "", "reinvoke-target-c51d7ba5-8eed-4226-99a6-6743f1169688", true}, + {"blank target", "reinvoke-handler-c51d7ba5-8eed-4226-99a6-6743f1169688", "", true}, } - for i, tt := range tests { - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Logf("\tTest: %d\tWhen checking %q for error status %v", i, tt.name, tt.wantErr) - { - c := &Scheduler{ - client: tt.fields.Client, - logger: logging.New("scheduler: "), - } - if err := c.CleanupEvents(tt.args.cloudWatchEventsRuleName, tt.args.cloudWatchEventsTargetID); (err != nil) != tt.wantErr { - t.Errorf("\t%s\tShould be able to make the cloudWatchEventsRuleName call : %v", Failed, err) - return - } - t.Logf("\t%s\tShould be able to make the cloudWatchEventsRuleName call.", Succeed) - + c := &Scheduler{ + client: NewMockEvents(), + logger: logging.New("scheduler: "), + } + if err := c.CleanupEvents(tt.rule, tt.target); (err != nil) != tt.wantErr { + t.Errorf("CleanupEvents() error = %v, wantErr %v", err, tt.wantErr) } }) } - } func TestGenerateCloudWatchIDS(t *testing.T) { - tests := []struct { - name string - WantRegxRuleName string - WantRegxTargetName string - WantRuleMatch bool - WantTargetMatch bool - wantErr bool - }{ - {"Test Rule and Target pattern", HandlerRegx, TargetRegx, true, true, false}, + ids, err := GenerateCloudWatchIDS() + if err != nil { + t.Fatalf("GenerateCloudWatchIDS() error = %v", err) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GenerateCloudWatchIDS() - if (err != nil) != tt.wantErr { - t.Errorf("GenerateCloudWatchIDS() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr == false { - matchedRule, err := regexp.Match(tt.WantRegxRuleName, []byte(got.Handler)) - - if (err != nil) != tt.wantErr { - t.Errorf("\t%s\tShould be able to make the Match call : %v", Failed, err) - return - } - t.Logf("\t%s\tShould be able to make the Matchcall.", Succeed) - - if matchedRule == tt.WantRuleMatch { - t.Logf("\t%s\tRule match should be (%v).", Succeed, tt.WantRuleMatch) - } else { - t.Errorf("\t%s\tRule match should be (%v). : %v", Failed, tt.WantRuleMatch, matchedRule) - } - - matchedTarget, err := regexp.Match(tt.WantRegxTargetName, []byte(got.Target)) - - if (err != nil) != tt.wantErr { - t.Errorf("\t%s\tShould be able to make the Match call : %v", Failed, err) - return - } - t.Logf("\t%s\tShould be able to make the Matchcall.", Succeed) - - if matchedTarget == tt.WantTargetMatch { - t.Logf("\t%s\tTarget match should be (%v).", Succeed, tt.WantTargetMatch) - } else { - t.Errorf("\t%s\tTarget match should be (%v). : %v", Failed, tt.WantRegxTargetName, matchedTarget) - } + matchedHandler, _ := regexp.MatchString(HandlerRegx, ids.Handler) + if !matchedHandler { + t.Errorf("Handler %q does not match pattern %s", ids.Handler, HandlerRegx) + } - } - }) + matchedTarget, _ := regexp.MatchString(TargetRegx, ids.Target) + if !matchedTarget { + t.Errorf("Target %q does not match pattern %s", ids.Target, TargetRegx) } } diff --git a/cfn/types_test.go b/cfn/types_test.go index b7d8855d..a54b0252 100644 --- a/cfn/types_test.go +++ b/cfn/types_test.go @@ -1,86 +1,66 @@ package cfn import ( + "context" + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "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" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" ) -// EmptyHandler is a implementation of Handler -// -// This implementation of the handlers is only used for testing. +// EmptyHandler is an implementation of Handler for testing. type EmptyHandler struct{} func (h *EmptyHandler) Create(request handler.Request) handler.ProgressEvent { return handler.ProgressEvent{} } - func (h *EmptyHandler) Read(request handler.Request) handler.ProgressEvent { return handler.ProgressEvent{} } - func (h *EmptyHandler) Update(request handler.Request) handler.ProgressEvent { return handler.ProgressEvent{} } - func (h *EmptyHandler) Delete(request handler.Request) handler.ProgressEvent { return handler.ProgressEvent{} } - func (h *EmptyHandler) List(request handler.Request) handler.ProgressEvent { return handler.ProgressEvent{} } -// MockHandler is a implementation of Handler -// -// This implementation of the handlers is only used for testing. +// MockHandler is an implementation of Handler for testing. type MockHandler struct { - fn func(callback map[string]interface{}, s *session.Session) handler.ProgressEvent + fn func(callback map[string]interface{}, cfg *aws.Config) handler.ProgressEvent } func (m *MockHandler) Create(request handler.Request) handler.ProgressEvent { - return m.fn(request.CallbackContext, request.Session) + return m.fn(request.CallbackContext, request.Config) } - func (m *MockHandler) Read(request handler.Request) handler.ProgressEvent { - return m.fn(request.CallbackContext, request.Session) + return m.fn(request.CallbackContext, request.Config) } - func (m *MockHandler) Update(request handler.Request) handler.ProgressEvent { - return m.fn(request.CallbackContext, request.Session) + return m.fn(request.CallbackContext, request.Config) } - func (m *MockHandler) Delete(request handler.Request) handler.ProgressEvent { - return m.fn(request.CallbackContext, request.Session) + return m.fn(request.CallbackContext, request.Config) } - func (m *MockHandler) List(request handler.Request) handler.ProgressEvent { - return m.fn(request.CallbackContext, request.Session) + return m.fn(request.CallbackContext, request.Config) } -// MockedMetrics mocks the call to AWS CloudWatch Metrics -// -// This implementation of the handlers is only used for testing. +// MockedMetrics mocks CloudWatch for testing. type MockedMetrics struct { - cloudwatchiface.CloudWatchAPI ResourceTypeName string HandlerExceptionCount int HandlerInvocationDurationCount int HandlerInvocationCount int } -// NewMockedMetrics is a factory function that returns a new MockedMetrics. -// -// This implementation of the handlers is only used for testing. func NewMockedMetrics() *MockedMetrics { return &MockedMetrics{} } -// PutMetricData mocks the PutMetricData method. -// -// This implementation of the handlers is only used for testing. -func (m *MockedMetrics) PutMetricData(in *cloudwatch.PutMetricDataInput) (*cloudwatch.PutMetricDataOutput, error) { +func (m *MockedMetrics) PutMetricData(ctx context.Context, in *cloudwatch.PutMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) { m.ResourceTypeName = *in.Namespace d := in.MetricData[0].MetricName switch *d { @@ -91,21 +71,16 @@ func (m *MockedMetrics) PutMetricData(in *cloudwatch.PutMetricDataInput) (*cloud case "HandlerInvocationCount": m.HandlerInvocationCount++ } - return nil, nil } -// MockModel mocks a resource model -// -// This implementation of the handlers is only used for testing. +// MockModel mocks a resource model for testing. type MockModel struct { Property1 *string `json:"property1,omitempty"` Property2 *string `json:"property2,omitempty"` } -// MockModelHandler is a implementation of Handler -// -// This implementation of the handlers is only used for testing. +// MockModelHandler is an implementation of Handler for testing. type MockModelHandler struct { fn func(r handler.Request) handler.ProgressEvent } @@ -113,19 +88,15 @@ type MockModelHandler struct { func (m *MockModelHandler) Create(request handler.Request) handler.ProgressEvent { return m.fn(request) } - func (m *MockModelHandler) Read(request handler.Request) handler.ProgressEvent { return m.fn(request) } - func (m *MockModelHandler) Update(request handler.Request) handler.ProgressEvent { return m.fn(request) } - func (m *MockModelHandler) Delete(request handler.Request) handler.ProgressEvent { return m.fn(request) } - func (m *MockModelHandler) List(request handler.Request) handler.ProgressEvent { return m.fn(request) } diff --git a/examples/github-repo/cmd/resource/resource.go b/examples/github-repo/cmd/resource/resource.go index f77c2e75..50af13c9 100644 --- a/examples/github-repo/cmd/resource/resource.go +++ b/examples/github-repo/cmd/resource/resource.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/google/go-github/github" "golang.org/x/oauth2" ) diff --git a/go.mod b/go.mod index 2d827003..14b1663e 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,37 @@ module github.com/aws-cloudformation/cloudformation-cli-go-plugin -go 1.19 +go 1.24 require ( github.com/avast/retry-go v2.7.0+incompatible - github.com/aws/aws-lambda-go v1.37.0 - github.com/aws/aws-sdk-go v1.44.197 - github.com/google/go-cmp v0.5.9 + github.com/aws/aws-lambda-go v1.49.0 + github.com/aws/aws-sdk-go-v2 v1.42.0 + github.com/aws/aws-sdk-go-v2/config v1.32.25 + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 + github.com/aws/aws-sdk-go-v2/service/cloudformation v1.72.1 + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.59.0 + github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.33.5 + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.76.0 + github.com/google/go-cmp v0.7.0 github.com/google/go-github v17.0.0+incompatible - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 github.com/segmentio/ksuid v1.0.4 - golang.org/x/oauth2 v0.5.0 + golang.org/x/oauth2 v0.25.0 gopkg.in/validator.v2 v2.0.1 ) require ( - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - golang.org/x/net v0.6.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.1 // indirect + github.com/google/go-querystring v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index ee7d6b46..ccb4562f 100644 --- a/go.sum +++ b/go.sum @@ -1,82 +1,71 @@ github.com/avast/retry-go v2.7.0+incompatible h1:XaGnzl7gESAideSjr+I8Hki/JBi+Yb9baHlMRPeSC84= github.com/avast/retry-go v2.7.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/aws/aws-lambda-go v1.37.0 h1:WXkQ/xhIcXZZ2P5ZBEw+bbAKeCEcb5NtiYpSwVVzIXg= -github.com/aws/aws-lambda-go v1.37.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= -github.com/aws/aws-sdk-go v1.44.197 h1:pkg/NZsov9v/CawQWy+qWVzJMIZRQypCtYjUBXFomF8= -github.com/aws/aws-sdk-go v1.44.197/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/aws/aws-lambda-go v1.49.0 h1:z4VhTqkFZPM3xpEtTqWqRqsRH4TZBMJqTkRiBPYLqIQ= +github.com/aws/aws-lambda-go v1.49.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/cloudformation v1.72.1 h1:vI/iCtlKp8LscUuwiWfTNID18w+naIGXKyWiY5odsZU= +github.com/aws/aws-sdk-go-v2/service/cloudformation v1.72.1/go.mod h1:67kQqAVkI9zNMo9kj1ca5RQvsDczK6xKCXrXn7ObZA8= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.59.0 h1:JOrwHweL6IzRjbDxdjup2YI2QjWa8/h0PGexR8MZpKw= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.59.0/go.mod h1:tsfAcBcMTF2G9UirQTP1In3DrkNO16SyUU527NPLPhs= +github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.33.5 h1:Plmh6Ac+VIHRj8jbRJalxLz7PGWSdCKpPcQNGqVe0BI= +github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.33.5/go.mod h1:VpBC7pUJj2oH5gRxEeMigmvGGynzdgH6bUZ4nDW2p+U= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.76.0 h1:wdP7t2nMLk/EzDTqVgBuBBvY7I461+kPn2+wUyQSelY= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.76.0/go.mod h1:N336OxQ6TvRbb6V1esVE8PtQFU86YvYaS+lVjsJTmP0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=