Skip to content

Commit

Permalink
Feat daily cost notification template
Browse files Browse the repository at this point in the history
  • Loading branch information
nao1215 committed Feb 21, 2024
1 parent 441b4a3 commit 3696053
Show file tree
Hide file tree
Showing 25 changed files with 812 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The s3hub command provides following features:
|:--|:--|:--|
|[Lambda batch with EventBridge (CloudWatch Events)](./cloudformation/lambda-batch/README.md)||100%|
|[Lambda with API Gateway](./cloudformation/lambda-with-api-gw/README.md)||100%|
|[Daily Cost Notification](./cloudformation/daily-cost-notification/README.md)||100%|
|[CloudWatch Real User Monitoring (RUM)](./cloudformation/cloudwatch-rum/README.md)||100%|


Expand Down
25 changes: 25 additions & 0 deletions app/domain/service/cost_explorer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package service

import (
"context"
"time"
)

// CostGetterInput is the interface that wraps the basic GetCost method.
type CostGetterInput struct {
// Start is the start date of the period.
Start time.Time
// End is the end date of the period.
End time.Time
}

// CostGetterOutput is the output of the GetCost method.
type CostGetterOutput struct {
// Cost is the cost of the period. The unit is USD.
Cost string
}

// CostGetter is the interface that wraps the basic GetCost method.
type CostGetter interface {
GetCost(ctx context.Context, input *CostGetterInput) (*CostGetterOutput, error)
}
19 changes: 19 additions & 0 deletions app/domain/service/sns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package service

import "context"

// SNSPublisherInput is the input of the Publish method.
type SNSPublisherInput struct {
// TopicArn is the ARN of the topic.
TopicArn string
// Message is the message that you want to publish.
Message string
}

// SNSPublisherOutput is the output of the Publish method.
type SNSPublisherOutput struct{}

// SNSPublisher is the interface that wraps the basic Publish method.
type SNSPublisher interface {
PublishSNS(ctx context.Context, input *SNSPublisherInput) (*SNSPublisherOutput, error)
}
61 changes: 61 additions & 0 deletions app/external/cost_explorer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package external

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/costexplorer"
"github.com/aws/aws-sdk-go-v2/service/costexplorer/types"
"github.com/google/wire"
"github.com/nao1215/rainbow/app/domain/model"
"github.com/nao1215/rainbow/app/domain/service"
)

// NewCostExplorerClient returns a new CostExplorer client.
func NewCostExplorerClient(cfg *model.AWSConfig) *costexplorer.Client {
return costexplorer.NewFromConfig(*cfg.Config)
}

// CostGetter is an interface for getting cost.
type CostGetter struct {
*costexplorer.Client
}

// CostGetterSet is a provider set for CostGetter.
//
//nolint:gochecknoglobals
var CostGetterSet = wire.NewSet(
NewCostGetter,
wire.Bind(new(service.CostGetter), new(*CostGetter)),
)

var _ service.CostGetter = (*CostGetter)(nil)

// NewCostGetter creates a new CostGetter.
func NewCostGetter(c *costexplorer.Client) *CostGetter {
return &CostGetter{Client: c}
}

// GetCost gets the cost.
func (c *CostGetter) GetCost(ctx context.Context, input *service.CostGetterInput) (*service.CostGetterOutput, error) {
params := &costexplorer.GetCostAndUsageInput{
TimePeriod: &types.DateInterval{
Start: aws.String(input.Start.Format("2006-01-02")),
End: aws.String(input.End.Format("2006-01-02")),
},
Granularity: types.GranularityDaily,
Metrics: []string{"UnblendedCost"},
}

resp, err := c.GetCostAndUsage(ctx, params)
if err != nil {
return nil, err
}

if len(resp.ResultsByTime) == 0 || len(resp.ResultsByTime[0].Total) == 0 {
return nil, fmt.Errorf("no cost data available for the specified time period")
}

return &service.CostGetterOutput{Cost: *resp.ResultsByTime[0].Total["UnblendedCost"].Amount}, nil
}
15 changes: 15 additions & 0 deletions app/external/mock/cost_explorer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mock

import (
"context"

"github.com/nao1215/rainbow/app/domain/service"
)

// CostGetter is a mock of the CostGetter interface.
type CostGetter func(ctx context.Context, input *service.CostGetterInput) (*service.CostGetterOutput, error)

// GetCost calls the GetCostFunc.
func (m CostGetter) GetCost(ctx context.Context, input *service.CostGetterInput) (*service.CostGetterOutput, error) {
return m(ctx, input)
}
15 changes: 15 additions & 0 deletions app/external/mock/sns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mock

import (
"context"

"github.com/nao1215/rainbow/app/domain/service"
)

// SNSPublisher is a mock of the SNSPublisher interface.
type SNSPublisher func(ctx context.Context, input *service.SNSPublisherInput) (*service.SNSPublisherOutput, error)

// PublishSNS is a mock of the PublishSNS method.
func (m SNSPublisher) PublishSNS(ctx context.Context, input *service.SNSPublisherInput) (*service.SNSPublisherOutput, error) {
return m(ctx, input)
}
50 changes: 50 additions & 0 deletions app/external/sns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package external

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/google/wire"
"github.com/nao1215/rainbow/app/domain/model"
"github.com/nao1215/rainbow/app/domain/service"
)

// NewSNSClient returns a new SNSClient.
func NewSNSClient(cfg *model.AWSConfig) *sns.Client {
return sns.NewFromConfig(*cfg.Config)
}

// SNSPublisher is an implementation for SNSPublisher.
type SNSPublisher struct {
*sns.Client
}

// SNSPublisherSet is a provider set for SNSPublisher.
//
//nolint:gochecknoglobals
var SNSPublisherSet = wire.NewSet(
NewSNSPublisher,
wire.Bind(new(service.SNSPublisher), new(*SNSPublisher)),
)

// NewSNSPublisher creates a new SNSPublisher.
func NewSNSPublisher(c *sns.Client) *SNSPublisher {
return &SNSPublisher{
Client: c,
}
}

var _ service.SNSPublisher = (*SNSPublisher)(nil)

// PublishSNS publishes a message to SNS.
func (p *SNSPublisher) PublishSNS(ctx context.Context, input *service.SNSPublisherInput) (*service.SNSPublisherOutput, error) {
if _, err := p.Publish(ctx, &sns.PublishInput{
Message: aws.String(input.Message),
TopicArn: aws.String(input.TopicArn),
}); err != nil {
return nil, err
}

return &service.SNSPublisherOutput{}, nil
}
45 changes: 45 additions & 0 deletions app/interactor/cost_explorer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package interactor

import (
"context"
"errors"

"github.com/google/wire"
"github.com/nao1215/rainbow/app/domain/service"
"github.com/nao1215/rainbow/app/usecase"
)

// CostGetterSet is a set of CostGetter.
//
//nolint:gochecknoglobals
var CostGetterSet = wire.NewSet(
NewCostGetter,
wire.Bind(new(usecase.CostGetter), new(*CostGetter)),
)

var _ usecase.CostGetter = (*CostGetter)(nil)

// CostGetter is an implementation for CostGetter.
type CostGetter struct {
service.CostGetter
}

// NewCostGetter returns a new CostGetter struct.
func NewCostGetter(c service.CostGetter) *CostGetter {
return &CostGetter{CostGetter: c}
}

// GetCost gets the cost.
func (c *CostGetter) GetCost(ctx context.Context, input *usecase.CostGetterInput) (*usecase.CostGetterOutput, error) {
if input.End.Before(input.Start) {
return nil, errors.New("End date is before the start date")
}
output, err := c.CostGetter.GetCost(ctx, &service.CostGetterInput{
Start: input.Start,
End: input.End,
})
if err != nil {
return nil, err
}
return &usecase.CostGetterOutput{Cost: output.Cost}, nil
}
69 changes: 69 additions & 0 deletions app/interactor/cost_explorer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package interactor

import (
"context"
"errors"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/nao1215/rainbow/app/domain/service"
"github.com/nao1215/rainbow/app/external/mock"
"github.com/nao1215/rainbow/app/usecase"
)

func TestCostGetter_GetCost(t *testing.T) {
t.Run("Success to get cost", func(t *testing.T) {
t.Parallel()

costGetter := mock.CostGetter(func(_ context.Context, input *service.CostGetterInput) (*service.CostGetterOutput, error) {
want := &service.CostGetterInput{
Start: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2021, 1, 31, 23, 59, 59, 0, time.UTC),
}

if diff := cmp.Diff(want, input); diff != "" {
t.Errorf("differs: (-want +got)\n%s", diff)
}
return &service.CostGetterOutput{Cost: "1000"}, nil
})

getter := NewCostGetter(costGetter)
got, err := getter.GetCost(context.Background(), &usecase.CostGetterInput{
Start: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2021, 1, 31, 23, 59, 59, 0, time.UTC),
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := &usecase.CostGetterOutput{Cost: "1000"}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("differs: (-want +got)\n%s", diff)
}
})

t.Run("Fail to get cost", func(t *testing.T) {
t.Parallel()

costGetter := mock.CostGetter(func(_ context.Context, _ *service.CostGetterInput) (*service.CostGetterOutput, error) {
want := &service.CostGetterInput{
Start: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2021, 1, 31, 23, 59, 59, 0, time.UTC),
}
if diff := cmp.Diff(want, want); diff != "" {
t.Errorf("differs: (-want +got)\n%s", diff)
}
return nil, errors.New("failed to get cost")
})

getter := NewCostGetter(costGetter)
_, err := getter.GetCost(context.Background(), &usecase.CostGetterInput{
Start: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2021, 1, 31, 23, 59, 59, 0, time.UTC),
})
if err == nil {
t.Error("expected error, but not occurred")
}
})
}
40 changes: 40 additions & 0 deletions app/interactor/sns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package interactor

import (
"context"

"github.com/google/wire"
"github.com/nao1215/rainbow/app/domain/service"
"github.com/nao1215/rainbow/app/usecase"
)

// SNSPublisherSet is a set of SNSPublisher.
//
//nolint:gochecknoglobals
var SNSPublisherSet = wire.NewSet(
NewSNSPublisher,
wire.Bind(new(usecase.SNSPublisher), new(*SNSPublisher)),
)

var _ usecase.SNSPublisher = (*SNSPublisher)(nil)

// SNSPublisher is an implementation for SNSPublisher.
type SNSPublisher struct {
service.SNSPublisher
}

// NewSNSPublisher returns a new SNSPublisher struct.
func NewSNSPublisher(s service.SNSPublisher) *SNSPublisher {
return &SNSPublisher{SNSPublisher: s}
}

// PublishSNS publishes a message to SNS.
func (s *SNSPublisher) PublishSNS(ctx context.Context, input *usecase.SNSPublisherInput) (*usecase.SNSPublisherOutput, error) {
if _, err := s.SNSPublisher.PublishSNS(ctx, &service.SNSPublisherInput{
Message: input.Message,
TopicArn: input.TopicArn,
}); err != nil {
return nil, err
}
return &usecase.SNSPublisherOutput{}, nil
}
Loading

0 comments on commit 3696053

Please sign in to comment.