Skip to content

Commit

Permalink
Merge pull request #42 from nao1215/feat/cfn-list
Browse files Browse the repository at this point in the history
Add cloudformation(cfn) ls subcommand
  • Loading branch information
nao1215 authored Jan 24, 2024
2 parents 299ff0b + 4886caf commit 6a54581
Show file tree
Hide file tree
Showing 16 changed files with 784 additions and 74 deletions.
25 changes: 25 additions & 0 deletions app/di/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,28 @@ func newSpareApp(
S3BucketPolicySetter: s3BucketPolicySetter,
}
}

// CFnApp is the application service for CloudFormation.
type CFnApp struct {
// CFnStackLister is the usecase for listing CloudFormation stacks.
usecase.CFnStackLister
}

// NewCFnApp creates a new CFnApp.
func NewCFnApp(ctx context.Context, profile model.AWSProfile, region model.Region) (*CFnApp, error) {
wire.Build(
model.NewAWSConfig,
external.NewCloudFormationClient,
external.CFnStackListerSet,
interactor.CFnStackListerSet,
newCFnApp,
)
return nil, nil
}

// newCFnApp creates a new CFnApp.
func newCFnApp(cFnStackLister usecase.CFnStackLister) *CFnApp {
return &CFnApp{
CFnStackLister: cFnStackLister,
}
}
27 changes: 27 additions & 0 deletions app/di/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 97 additions & 0 deletions app/domain/model/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,103 @@ const (
// StackStatus is the status of a CloudFormation stack.
type StackStatus string

// String returns the string representation of StackStatus.
func (s StackStatus) String() string {
return string(s)
}

// CloudFormation stack status constants
// Ref. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
const (
// StackStatusCreateInProgress is ongoing creation of one or more stacks.
StackStatusCreateInProgress StackStatus = "CREATE_IN_PROGRESS"
// StackStatusCreateComplete is successful creation of one or more stacks.
StackStatusCreateComplete StackStatus = "CREATE_COMPLETE"
// StackStatusCreateFailed is unsuccessful creation of one or more stacks.
// View the stack events to see any associated error messages. Possible reasons
// for a failed creation include insufficient permissions to work with all
// resources in the stack, parameter values rejected by an AWS service, or a
// timeout during resource creation.
StackStatusCreateFailed StackStatus = "CREATE_FAILED"

// StackStatusRollbackInProgress is ongoing removal of one or more stacks after a failed
// stack creation or after an explicitly canceled stack creation.
StackStatusRollbackInProgress StackStatus = "ROLLBACK_IN_PROGRESS"
// StackStatusRollbackComplete is successful removal of one or more stacks after a failed
// stack creation or after an explicitly canceled stack creation. The stack returns to
// the previous working state. Any resources that were created during the create stack
// operation are deleted.
// This status exists only after a failed stack creation. It signifies that all operations
// from the partially created stack have been appropriately cleaned up. When in this state,
// only a delete operation can be performed.
StackStatusRollbackComplete StackStatus = "ROLLBACK_COMPLETE"
// StackStatusRollbackFailed is unsuccessful removal of one or more stacks after a failed
// stack creation or after an explicitly canceled stack creation. Delete the stack or view
// the stack events to see any associated error messages.
StackStatusRollbackFailed StackStatus = "ROLLBACK_FAILED"

// StackStatusDeleteInProgress is ongoing removal of one or more stacks.
StackStatusDeleteInProgress StackStatus = "DELETE_IN_PROGRESS"
// StackStatusDeleteComplete is successful deletion of one or more stacks.
// Deleted stacks are retained and viewable for 90 days.
StackStatusDeleteComplete StackStatus = "DELETE_COMPLETE"
// StackStatusDeleteFailed is unsuccessful deletion of one or more stacks.
// Because the delete failed, you might have some resources that are still
// running; however, you can't work with or update the stack. Delete the stack
// again or view the stack events to see any associated error messages.
StackStatusDeleteFailed StackStatus = "DELETE_FAILED"

// StackStatusUpdateInProgress is ongoing creation of one or more stacks with
// an expected StackId but without any templates or resources.
// A stack with this status code counts against the maximum possible number of stacks.
StackStatusReviewInProgress StackStatus = "REVIEW_IN_PROGRESS"
// StackStatusUpdateInProgress is ongoing update of one or more stacks.
StackStatusUpdateInProgress StackStatus = "UPDATE_IN_PROGRESS"
// StackStatusUpdateCompleteCleanupInProgress is ongoing removal of old resources for
// one or more stacks after a successful stack update. For stack updates that require
// resources to be replaced, CloudFormation creates the new resources first and then
// deletes the old resources to help reduce any interruptions with your stack. In this
// state, the stack has been updated and is usable, but CloudFormation is still deleting
// the old resources.
StackStatusUpdateCompleteCleanupInProgress StackStatus = "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"
// StackStatusUpdateComplete is successful update of one or more stacks.
StackStatusUpdateComplete StackStatus = "UPDATE_COMPLETE"
// StackStatusUpdateFailed is unsuccessful update of one or more stacks. View the stack events
// to see any associated error messages.
StackStatusUpdateFailed StackStatus = "UPDATE_FAILED"
// StackStatusUpdateRollbackComplete is successful return of one or more stacks to a previous
// working state after a failed stack update.
StackStatusUpdateRollbackComplete StackStatus = "UPDATE_ROLLBACK_COMPLETE"
// StackStatusUpdateRollbackCompleteCleanupInProgress is ongoing removal of new resources
// for one or more stacks after a failed stack update. In this state, the stack has been
// rolled back to its previous working state and is usable, but CloudFormation is still
// deleting any new resources it created during the stack update.
StackStatusUpdateRollbackCompleteCleanupInProgress StackStatus = "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"
// StackStatusUpdateRollbackFailed is unsuccessful return of one or more stacks to a
// previous working state after a failed stack update. When in this state, you can
// delete the stack or continue rollback. You might need to fix errors before your
// stack can return to a working state.
StackStatusUpdateRollbackFailed StackStatus = "UPDATE_ROLLBACK_FAILED"
// StackStatusUpdateRollbackInProgress is ongoing return of one or more stacks
// to the previous working state after failed stack update.
StackStatusUpdateRollbackInProgress StackStatus = "UPDATE_ROLLBACK_IN_PROGRESS"

// StackStatusImportInProgress is the import operation is currently in progress.
StackStatusImportInProgress StackStatus = "IMPORT_IN_PROGRESS"
// StackStatusImportComplete is the import operation successfully completed for
// all resources in the stack that support resource import.
StackStatusImportComplete StackStatus = "IMPORT_COMPLETE"
// StackStatusImportRollbackInProgress is import will roll back to the previous
// template configuration.
StackStatusImportRollbackInProgress StackStatus = "IMPORT_ROLLBACK_IN_PROGRESS"
// StackStatusImportRollbackComplete is import successfully rolled back to the previous template configuration.
StackStatusImportRollbackComplete StackStatus = "IMPORT_ROLLBACK_COMPLETE"
// StackStatusImportRollbackFailed is the import rollback operation failed for at
// least one resource in the stack. Results will be available for the resources
// CloudFormation successfully imported.
StackStatusImportRollbackFailed StackStatus = "IMPORT_ROLLBACK_FAILED"
)

// StackDriftInformationSummary contains information about whether the stack's
// actual configuration differs, or has drifted, from its expected configuration,
// as defined in the stack template and any values specified as template parameters.
Expand Down
7 changes: 6 additions & 1 deletion app/domain/service/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import (
)

// CFnStackListerInput is the input of the CFnStackLister method.
type CFnStackListerInput struct{}
type CFnStackListerInput struct {
// Region is the region of the stack.
Region model.Region
}

// CFnStackListerOutput is the output of the CFnStackLister method.
type CFnStackListerOutput struct {
Expand All @@ -24,6 +27,8 @@ type CFnStackLister interface {
type CFnStackResourceListerInput struct {
// StackName is the name of the stack.
StackName string
// Region is the region of the stack.
Region model.Region
}

// CFnStackResourceListerOutput is the output of the CFnStackResourceLister method.
Expand Down
16 changes: 11 additions & 5 deletions app/external/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ func NewCFnStackLister(client *cloudformation.Client) *CFnStackLister {
}

// CFnStackLister returns a list of CloudFormation stacks.
func (l *CFnStackLister) CFnStackLister(ctx context.Context, _ *service.CFnStackListerInput) (*service.CFnStackListerOutput, error) {
func (l *CFnStackLister) CFnStackLister(ctx context.Context, input *service.CFnStackListerInput) (*service.CFnStackListerOutput, error) {
in := &cloudformation.ListStacksInput{}
stacks := make([]*model.Stack, 0, 100)
opt := func(o *cloudformation.Options) {
o.Region = input.Region.String()
}

stacks := make([]*model.Stack, 0, 100)
for {
select {
case <-ctx.Done():
Expand All @@ -60,7 +63,7 @@ func (l *CFnStackLister) CFnStackLister(ctx context.Context, _ *service.CFnStack
default:
}

out, err := l.client.ListStacks(ctx, in)
out, err := l.client.ListStacks(ctx, in, opt)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -120,8 +123,11 @@ func (l *CFnStackResourceLister) CFnStackResourceLister(ctx context.Context, inp
in := &cloudformation.ListStackResourcesInput{
StackName: aws.String(input.StackName),
}
resources := make([]*model.StackResource, 0, 100)
opt := func(o *cloudformation.Options) {
o.Region = input.Region.String()
}

resources := make([]*model.StackResource, 0, 100)
for {
select {
case <-ctx.Done():
Expand All @@ -131,7 +137,7 @@ func (l *CFnStackResourceLister) CFnStackResourceLister(ctx context.Context, inp
default:
}

out, err := l.client.ListStackResources(ctx, in)
out, err := l.client.ListStackResources(ctx, in, opt)
if err != nil {
return nil, err
}
Expand Down
15 changes: 15 additions & 0 deletions app/external/mock/cloudformation.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"
)

// CFnStackLister is a mock of the CFnStackLister interface.
type CFnStackLister func(ctx context.Context, input *service.CFnStackListerInput) (*service.CFnStackListerOutput, error)

// CFnStackLister calls the CFnStackListerFunc.
func (m CFnStackLister) CFnStackLister(ctx context.Context, input *service.CFnStackListerInput) (*service.CFnStackListerOutput, error) {
return m(ctx, input)
}
44 changes: 44 additions & 0 deletions app/interactor/cloudformation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package interactor

import (
"context"

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

// CFnStackListerSet is a set of CFnStackLister.
//
//nolint:gochecknoglobals
var CFnStackListerSet = wire.NewSet(
NewCFnStackLister,
wire.Bind(new(usecase.CFnStackLister), new(*CFnStackLister)),
)

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

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

// NewCFnStackLister returns a new CFnStackLister struct.
func NewCFnStackLister(lister service.CFnStackLister) *CFnStackLister {
return &CFnStackLister{
CFnStackLister: lister,
}
}

// ListCFnStack returns a list of CloudFormation stacks.
func (l *CFnStackLister) ListCFnStack(ctx context.Context, input *usecase.CFnStackListerInput) (*usecase.CFnStackListerOutput, error) {
output, err := l.CFnStackLister.CFnStackLister(ctx, &service.CFnStackListerInput{
Region: input.Region,
})
if err != nil {
return nil, err
}
return &usecase.CFnStackListerOutput{
Stacks: output.Stacks,
}, nil
}
84 changes: 84 additions & 0 deletions app/interactor/cloudformation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package interactor

import (
"context"
"errors"
"testing"

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

func TestCFnStackLister_ListCFnStack(t *testing.T) {
t.Parallel()

t.Run("success to get cloudformation stack list", func(t *testing.T) {
t.Parallel()

stackLister := mock.CFnStackLister(func(ctx context.Context, input *service.CFnStackListerInput) (*service.CFnStackListerOutput, error) {
want := &service.CFnStackListerInput{
Region: model.RegionAPEast1,
}
if diff := cmp.Diff(want, input); diff != "" {
t.Errorf("differs: (-want +got)\n%s", diff)
}

return &service.CFnStackListerOutput{
Stacks: []*model.Stack{
{
StackName: aws.String("stackName1"),
StackID: aws.String(model.StackStatusCreateComplete.String()),
},
{
StackName: aws.String("stackName2"),
StackID: aws.String(model.StackStatusCreateComplete.String()),
},
},
}, nil
})

lister := NewCFnStackLister(stackLister)
output, err := lister.ListCFnStack(context.Background(), &usecase.CFnStackListerInput{
Region: model.RegionAPEast1,
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := &usecase.CFnStackListerOutput{
Stacks: []*model.Stack{
{
StackName: aws.String("stackName1"),
StackID: aws.String(model.StackStatusCreateComplete.String()),
},
{
StackName: aws.String("stackName2"),
StackID: aws.String(model.StackStatusCreateComplete.String()),
},
},
}
if diff := cmp.Diff(want, output); diff != "" {
t.Errorf("differs: (-want +got)\n%s", diff)
}
})

t.Run("fail to get cloudformation stack list", func(t *testing.T) {
t.Parallel()

stackLister := mock.CFnStackLister(func(ctx context.Context, input *service.CFnStackListerInput) (*service.CFnStackListerOutput, error) {
return nil, errors.New("some error")
})

lister := NewCFnStackLister(stackLister)
_, err := lister.ListCFnStack(context.Background(), &usecase.CFnStackListerInput{
Region: model.RegionAPEast1,
})
if err == nil {
t.Error("expected error, but nil")
}
})
}
Loading

0 comments on commit 6a54581

Please sign in to comment.