-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.go
214 lines (184 loc) · 5.79 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// Stage is a single step in a deployment process. Only one stage can be running at one time,
// And the entire process exits if any stage fails along the way
// The Action is the function that is run to complete the stage's work
// IsComplete
type Stage struct {
Name string
Action func() error
Error error
IsComplete bool
IsCompleteFunc func() bool
Reset func() error
}
var stageIndex = 0
var stages = []Stage{
{
Name: "One",
Action: func() error {
time.Sleep(3 * time.Second)
return nil
},
IsCompleteFunc: func() bool { return false },
IsComplete: false,
},
{
Name: "Two",
Action: func() error {
time.Sleep(3 * time.Second)
return errors.New("This one errored")
},
IsCompleteFunc: func() bool { return false },
IsComplete: false,
},
{
Name: "Three",
Action: func() error {
time.Sleep(3 * time.Second)
return nil
},
IsCompleteFunc: func() bool { return false },
IsComplete: false,
},
}
type model struct {
status int
Error error
spinner spinner.Model
}
type startDeployMsg struct{}
func startDeployCmd() tea.Msg {
return startDeployMsg{}
}
func runStage() tea.Msg {
if !stages[stageIndex].IsCompleteFunc() {
// Run the current stage, and record its result status
stages[stageIndex].Error = stages[stageIndex].Action()
}
return stageCompleteMsg{}
}
type stageCompleteMsg struct{}
type errMsg struct{ err error }
// For messages that contain errors it's often handy to also implement the
// error interface on the message.
func (e errMsg) Error() string { return e.err.Error() }
func initialModel() model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return model{
spinner: s,
}
}
func (m model) Init() tea.Cmd {
return tea.Batch(m.spinner.Tick, startDeployCmd)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case stageCompleteMsg:
// If we have an error, then set the error so that the views can properly update
if stages[stageIndex].Error != nil {
m.Error = stages[stageIndex].Error
writeCommandLogFile()
return m, tea.Quit
}
// Otherwise, mark the current stage as complete and move to the next stage
stages[stageIndex].IsComplete = true
// If we've reached the end of the defined stages, we're done
if stageIndex+1 >= len(stages) {
return m, tea.Quit
}
stageIndex++
return m, runStage
case errMsg:
m.Error = msg
return m, tea.Quit
case tea.KeyMsg:
if msg.Type == tea.KeyCtrlC {
return m, tea.Quit
}
case startDeployMsg:
return m, runStage
}
var spinnerCmd tea.Cmd
m.spinner, spinnerCmd = m.spinner.Update(msg)
return m, spinnerCmd
}
func renderCheckbox(s Stage) string {
sb := strings.Builder{}
if s.Error != nil {
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render(" ❌ "))
} else if s.IsComplete {
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("170")).Render(" ✅ "))
} else {
sb.WriteString(" 🔲 ")
}
return sb.String()
}
func renderWorkingStatus(m model, s Stage) string {
sb := strings.Builder{}
if !s.IsComplete {
sb.WriteString(m.spinner.View())
} else {
sb.WriteString(" ")
}
sb.WriteString(" ")
sb.WriteString(s.Name)
return sb.String()
}
func (m model) View() string {
sb := strings.Builder{}
sb.WriteString(fmt.Sprintf("Current stage: %s\n", stages[stageIndex].Name))
for _, stage := range stages {
sb.WriteString(renderCheckbox(stage) + " " + renderWorkingStatus(m, stage) + "\n")
}
return sb.String()
}
// commandLog is rendered when the deployment encounters an error. It retains a log of all the "commands" that were run in the course of deploying the example
// "commands" are intentionally in air-quotes here because this also includes things like checking for the existence of environment variables, and is not yet
// implemented in a truly re-windable cross-platform way, but it's a start, and it's better than asking someone over an email what failed
var commandLog = []string{}
func logCommand(s string) {
commandLog = append(commandLog, s)
}
func writeCommandLogFile() {
//Write the entire command log to a file on the filesystem so that the user has the option of sending it to Gruntwork for debugging purposes
// We currently write the file to ./gruntwork-examples-debug.log in the same directory as the executable was run in
// Create the file
f, err := os.Create("bubbletea-debug.log")
if err != nil {
fmt.Println(err)
return
}
// Write to the file, first writing the UTC timestamp as the first line, then looping through the command log to write each command on a new line
f.WriteString("Ran at: " + time.Now().UTC().String() + "\n")
f.WriteString("******************************************************************************\n")
f.WriteString("Human legible log of steps taken and commands run up to the point of failure:\n")
f.WriteString("******************************************************************************\n")
for _, cmd := range commandLog {
f.WriteString(cmd + "\n")
}
f.WriteString("^ The above command is likely the one that caused the error!\n")
f.WriteString("\n\n")
f.WriteString("******************************************************************************\n")
f.WriteString("Complete log of the error that halted the deployment:\n")
f.WriteString("******************************************************************************\n")
f.WriteString("\n\n")
f.WriteString(stages[stageIndex].Error.Error() + "\n")
}
func main() {
if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
fmt.Printf("Uh oh, there was an error: %v\n", err)
os.Exit(1)
}
}