Skip to content

Commit

Permalink
Introduce tui for listing up cloudformation stack
Browse files Browse the repository at this point in the history
  • Loading branch information
nao1215 committed Feb 24, 2024
1 parent 8f5a037 commit 3b9bbc3
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 56 deletions.
60 changes: 59 additions & 1 deletion app/domain/model/cloudformation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package model

import "time"
import (
"time"

"github.com/fatih/color"
)

const (
// CloudFormationRetryMaxAttempts is the maximum number of retries for CloudFormation.
Expand All @@ -18,6 +22,60 @@ func (s StackStatus) String() string {
return string(s)
}

// StringWithColor returns the string representation of StackStatus with color.
func (s StackStatus) StringWithColor() string {
switch s {
case StackStatusCreateComplete:
return color.GreenString("CREATE_COMPLETE")
case StackStatusCreateFailed:
return color.RedString("CREATE_FAILED")
case StackStatusCreateInProgress:
return color.YellowString("CREATE_IN_PROGRESS")
case StackStatusDeleteComplete:
return color.GreenString("DELETE_COMPLETE")
case StackStatusDeleteFailed:
return color.RedString("DELETE_FAILED")
case StackStatusDeleteInProgress:
return color.YellowString("DELETE_IN_PROGRESS")
case StackStatusRollbackComplete:
return color.GreenString("ROLLBACK_COMPLETE")
case StackStatusRollbackFailed:
return color.RedString("ROLLBACK_FAILED")
case StackStatusRollbackInProgress:
return color.YellowString("ROLLBACK_IN_PROGRESS")
case StackStatusUpdateComplete:
return color.GreenString("UPDATE_COMPLETE")
case StackStatusUpdateCompleteCleanupInProgress:
return color.YellowString("UPDATE_COMPLETE_CLEANUP_IN_PROGRESS")
case StackStatusUpdateInProgress:
return color.YellowString("UPDATE_IN_PROGRESS")
case StackStatusUpdateRollbackComplete:
return color.GreenString("UPDATE_ROLLBACK_COMPLETE")
case StackStatusUpdateRollbackCompleteCleanupInProgress:
return color.YellowString("UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS")
case StackStatusUpdateRollbackFailed:
return color.RedString("UPDATE_ROLLBACK_FAILED")
case StackStatusUpdateFailed:
return color.RedString("UPDATE_FAILED")
case StackStatusUpdateRollbackInProgress:
return color.YellowString("UPDATE_ROLLBACK_IN_PROGRESS")
case StackStatusReviewInProgress:
return color.YellowString("REVIEW_IN_PROGRESS")
case StackStatusImportInProgress:
return color.YellowString("IMPORT_IN_PROGRESS")
case StackStatusImportComplete:
return color.GreenString("IMPORT_COMPLETE")
case StackStatusImportRollbackInProgress:
return color.YellowString("IMPORT_ROLLBACK_IN_PROGRESS")
case StackStatusImportRollbackFailed:
return color.RedString("IMPORT_ROLLBACK_FAILED")
case StackStatusImportRollbackComplete:
return color.GreenString("IMPORT_ROLLBACK_COMPLETE")
default:
return color.RedString("UNKNOWN")
}
}

// CloudFormation stack status constants
// Ref. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
const (
Expand Down
8 changes: 8 additions & 0 deletions cmd/subcmd/cfn/interactive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package cfn

import tui "github.com/nao1215/rainbow/ui/cfn"

// interactive starts cfn command interactive UI.
func interactive() error {
return tui.RunCfnUI()
}
60 changes: 5 additions & 55 deletions cmd/subcmd/cfn/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,64 +52,14 @@ func (l *lsCmd) Do() error {
}

for _, stack := range out.Stacks {
if stack.StackName == nil || stack.StackStatus == model.StackStatusDeleteComplete {
continue
}

l.printf(" %s (status=%s, updated_at=%s)\n",
color.GreenString(*stack.StackName),
stackStatusString(stack.StackStatus),
stack.StackStatus.StringWithColor(),
stack.LastUpdatedTime.Format("2006-01-02 15:04:05"))
}
return nil
}

// stackStatusString returns a string representation of the stack status.
func stackStatusString(status model.StackStatus) string {
switch status {
case model.StackStatusCreateComplete:
return color.GreenString("CREATE_COMPLETE")
case model.StackStatusCreateFailed:
return color.RedString("CREATE_FAILED")
case model.StackStatusCreateInProgress:
return color.YellowString("CREATE_IN_PROGRESS")
case model.StackStatusDeleteComplete:
return color.GreenString("DELETE_COMPLETE")
case model.StackStatusDeleteFailed:
return color.RedString("DELETE_FAILED")
case model.StackStatusDeleteInProgress:
return color.YellowString("DELETE_IN_PROGRESS")
case model.StackStatusRollbackComplete:
return color.GreenString("ROLLBACK_COMPLETE")
case model.StackStatusRollbackFailed:
return color.RedString("ROLLBACK_FAILED")
case model.StackStatusRollbackInProgress:
return color.YellowString("ROLLBACK_IN_PROGRESS")
case model.StackStatusUpdateComplete:
return color.GreenString("UPDATE_COMPLETE")
case model.StackStatusUpdateCompleteCleanupInProgress:
return color.YellowString("UPDATE_COMPLETE_CLEANUP_IN_PROGRESS")
case model.StackStatusUpdateInProgress:
return color.YellowString("UPDATE_IN_PROGRESS")
case model.StackStatusUpdateRollbackComplete:
return color.GreenString("UPDATE_ROLLBACK_COMPLETE")
case model.StackStatusUpdateRollbackCompleteCleanupInProgress:
return color.YellowString("UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS")
case model.StackStatusUpdateRollbackFailed:
return color.RedString("UPDATE_ROLLBACK_FAILED")
case model.StackStatusUpdateFailed:
return color.RedString("UPDATE_FAILED")
case model.StackStatusUpdateRollbackInProgress:
return color.YellowString("UPDATE_ROLLBACK_IN_PROGRESS")
case model.StackStatusReviewInProgress:
return color.YellowString("REVIEW_IN_PROGRESS")
case model.StackStatusImportInProgress:
return color.YellowString("IMPORT_IN_PROGRESS")
case model.StackStatusImportComplete:
return color.GreenString("IMPORT_COMPLETE")
case model.StackStatusImportRollbackInProgress:
return color.YellowString("IMPORT_ROLLBACK_IN_PROGRESS")
case model.StackStatusImportRollbackFailed:
return color.RedString("IMPORT_ROLLBACK_FAILED")
case model.StackStatusImportRollbackComplete:
return color.GreenString("IMPORT_ROLLBACK_COMPLETE")
default:
return color.RedString("UNKNOWN")
}
}
5 changes: 5 additions & 0 deletions cmd/subcmd/cfn/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
package cfn

import (
"os"

"github.com/spf13/cobra"
)

// Execute starts the root command of cfn command.
func Execute() error {
if len(os.Args) == 1 {
return interactive()
}
if err := newRootCmd().Execute(); err != nil {
return err
}
Expand Down
30 changes: 30 additions & 0 deletions ui/cfn/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cfn

import (
"context"

tea "github.com/charmbracelet/bubbletea"
"github.com/nao1215/rainbow/app/di"
"github.com/nao1215/rainbow/app/domain/model"
"github.com/nao1215/rainbow/app/usecase"
"github.com/nao1215/rainbow/ui"
)

// fetchStacks is the message that is sent when the user wants to fetch the list of the CloudFormation stacks.
type fetchStacks struct {
stacks []*model.Stack
}

func fetchStacksCmd(ctx context.Context, app *di.CFnApp, region model.Region) tea.Cmd {
return tea.Cmd(func() tea.Msg {
output, err := app.CFnStackLister.ListCFnStack(ctx, &usecase.CFnStackListerInput{
Region: region,
})
if err != nil {
return ui.ErrMsg(err)
}
return fetchStacks{
stacks: output.Stacks,
}
})
}
197 changes: 197 additions & 0 deletions ui/cfn/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package cfn

import (
"context"
"fmt"

tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/nao1215/rainbow/app/di"
"github.com/nao1215/rainbow/app/domain/model"
"github.com/nao1215/rainbow/ui"
)

// cfnListStackModel is the model for listing the CloudFormation stacks.
type cfnListStackModel struct {
// err is the error that occurred during the operation.
err error
// awsConfig is the AWS configuration.
awsConfig *model.AWSConfig
// awsProfile is the AWS profile.
awsProfile model.AWSProfile
// region is the AWS region that the user wants to create the S3 bucket.
region model.Region
// choice is the currently selected menu item.
choice *ui.Choice
// app is the CFn application service.
app *di.CFnApp
// ctx is the context.
ctx context.Context
// stacks is the list of the CloudFormation stacks.
stacks []*model.Stack
// status is the status of the operation.
status status
// toggle is the currently selected menu item.
toggles ui.ToggleSets
}

const (
windowHeight = 10
)

// newCFnListStackModel returns the new cfnListStackModel for listing the CloudFormation stacks.
func newCFnListStackModel(region model.Region) (*cfnListStackModel, error) {
ctx := context.Background()
profile := model.NewAWSProfile("")
cfg, err := model.NewAWSConfig(ctx, profile, region)
if err != nil {
return nil, err
}

app, err := di.NewCFnApp(ctx, profile, region)
if err != nil {
return nil, err
}

return &cfnListStackModel{
awsConfig: cfg,
awsProfile: profile,
region: region,
app: app,
stacks: []*model.Stack{},
status: statusNone,
choice: ui.NewChoice(0, 0),
ctx: ctx,
}, nil
}

// Init initializes the model.
func (m *cfnListStackModel) Init() tea.Cmd {
return nil // Not called this method
}

// Update updates the model.
func (m *cfnListStackModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.err != nil {
return m, tea.Quit
}

switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "j", "down":
m.choice.Increment()
case "k", "up":
m.choice.Decrement()
case "ctrl+c":
m.status = statusQuit
return m, tea.Quit
case "q", "esc":
m.status = statusReturnToTop
return newCFnRootModel(m.awsProfile, m.awsConfig), nil
case "D":
// TODO: implement delete stack
case "enter":
if m.status == statusReturnToTop {
return newCFnRootModel(m.awsProfile, m.awsConfig), nil
}
case " ":
if m.status == statusStacksListed {
m.toggles[m.choice.Choice].Toggle()
}
}
case fetchStacks:
m.status = statusStacksFetched
m.stacks = make([]*model.Stack, 0, len(msg.stacks))
for _, stack := range msg.stacks {
if stack.StackName == nil || stack.StackStatus == model.StackStatusDeleteComplete {
continue
}
m.stacks = append(m.stacks, stack)
}
m.choice = ui.NewChoice(0, len(m.stacks)-1)
m.toggles = ui.NewToggleSets(len(m.stacks))
return m, nil
case ui.ErrMsg:
m.err = msg
m.status = statusQuit
return m, tea.Quit
default:
return m, nil
}
return m, nil
}

// View renders the application's UI.
func (m *cfnListStackModel) View() string {
if m.err != nil {
m.status = statusQuit
return ui.ErrorMessage(m.err)
}

switch m.status {
case statusQuit:
return ui.GoodByeMessage()
case statusNone, statusStacksFetching:
return fmt.Sprintf(
"fetching the list of the CloudForamtion Stack (profile=%s)\n",
m.awsProfile.String())
case statusStacksFetched:
return m.stackListString()
default:
return m.stackListString()
}
}

// stackListString returns the string of the list of the CloudFormation stacks.
func (m *cfnListStackModel) stackListString() string {
switch len(m.stacks) {
case 0:
return m.emptyStacksString()
default:
return m.stacksListStrWithCheckBox()
}
}

// emptyStacksString returns the string of the empty list of the CloudFormation stacks.
func (m *cfnListStackModel) emptyStacksString() string {
m.status = statusReturnToTop
return fmt.Sprintf("No CloudFormation Stacks (profile=%s)\n\n%s\n",
m.awsProfile.String(),
ui.Subtle("<enter>: return to the top"))
}

// stacksListStrWithCheckBox returns the string of the list of the CloudFormation stacks with checkbox.
func (m *cfnListStackModel) stacksListStrWithCheckBox() string {
startIndex := 0
endIndex := len(m.stacks)

if m.choice.Choice >= windowHeight {
startIndex = m.choice.Choice - windowHeight + 1
endIndex = startIndex + windowHeight
if endIndex > len(m.stacks) {
startIndex = len(m.stacks) - windowHeight
endIndex = len(m.stacks)
}
} else {
if len(m.stacks) > windowHeight {
endIndex = windowHeight
}
}

m.status = statusStacksListed
s := fmt.Sprintf("CloudForamtion Stacks %d/%d (profile=%s)\n\n", m.choice.Choice+1, len(m.stacks), m.awsProfile.String())
for i := startIndex; i < endIndex; i++ {
stack := m.stacks[i]
s += fmt.Sprintf("%s\n",
ui.ToggleWidget(
fmt.Sprintf(
" %s (status=%s, updated_at=%s)",
color.GreenString(*stack.StackName),
stack.StackStatus.StringWithColor(),
stack.LastUpdatedTime.Format("2006-01-02 15:04:05")),
m.choice.Choice == i, m.toggles[i].Enabled))
}
s += ui.Subtle("\n<esc>: return to the top | <Ctrl-C>: quit | up/down: select\n")
return s
}
Loading

0 comments on commit 3b9bbc3

Please sign in to comment.