Skip to content

Commit

Permalink
Merge pull request #246 from overmindtech/bubbletea-pt2
Browse files Browse the repository at this point in the history
Enhanced `terraform plan` and `terraform apply` commands
  • Loading branch information
DavidS-ovm authored May 3, 2024
2 parents 4a2c27d + b6bbab9 commit e1f6dc8
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 80 deletions.
4 changes: 2 additions & 2 deletions .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ exclude_unchanged = false
follow_symlink = false
# contrary to other repos, this does wait for the debugger to attach, as cli processes are very shortlived
# full_bin = "dlv exec --accept-multiclient --continue --headless --listen :9087 --api-version 2 ./tmp/overmind -- terraform plan"
# full_bin = "dlv exec --accept-multiclient --headless --listen :9087 --api-version 2 --tty /dev/pts/3 ./tmp/overmind -- terraform plan"
full_bin = "docker run --rm -e AWS_PROFILE=sso-david -e OVM_API_KEY -e APP -e RESET_STORED_CONFIG=true -e TEABUG=true -e LOG=trace -v $PWD:/vhs -v ~/.aws:/root/.aws ghcr.io/charmbracelet/vhs /vhs/demos/plan.tape && code demos/plan.gif"
# full_bin = "dlv exec --accept-multiclient --headless --listen :9087 --api-version 2 ./tmp/overmind -- terraform plan"
full_bin = "docker run --rm -e AWS_PROFILE=sso-david -e OVM_API_KEY -e APP=https://df.overmind-demo.com -e RESET_STORED_CONFIG=true -e TEABUG=true -e LOG=trace -v $PWD:/vhs -v ~/.aws:/root/.aws ghcr.io/charmbracelet/vhs /vhs/demos/plan.tape && code demos/plan.gif"
include_dir = []
include_ext = ["go", "tpl", "tmpl", "templ", "html", "sql", "css", "md", "tape"]
include_file = ["sqlc.yaml"]
Expand Down
12 changes: 12 additions & 0 deletions cmd/changes_submit_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,24 @@ func TestMappedItemDiffsFromPlan(t *testing.T) {

// t.Logf("item: %v", item.Attributes.AttrStruct.Fields["terraform_address"].GetStringValue())
if item.GetAttributes().GetAttrStruct().GetFields()["terraform_address"].GetStringValue() == "kubernetes_deployment.nats_box" {
if nats_box_deployment != nil {
t.Errorf("Found multiple nats_box_deployment: %v, %v", nats_box_deployment, diff)
}
nats_box_deployment = diff
} else if item.GetAttributes().GetAttrStruct().GetFields()["terraform_address"].GetStringValue() == "kubernetes_deployment.api_server" {
if api_server_deployment != nil {
t.Errorf("Found multiple api_server_deployment: %v, %v", api_server_deployment, diff)
}
api_server_deployment = diff
} else if item.GetType() == "iam-policy" {
if aws_iam_policy != nil {
t.Errorf("Found multiple aws_iam_policy: %v, %v", aws_iam_policy, diff)
}
aws_iam_policy = diff
} else if item.GetType() == "Secret" {
if secret != nil {
t.Errorf("Found multiple secrets: %v, %v", secret, diff)
}
secret = diff
}
}
Expand Down
18 changes: 11 additions & 7 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,34 +70,38 @@ func NewOvermindInstance(ctx context.Context, app string) (OvermindInstance, err
instanceDataUrl := fmt.Sprintf("%v/api/public/instance-data", instance.FrontendUrl)
req, err := http.NewRequest("GET", instanceDataUrl, nil)
if err != nil {
log.WithError(err).Fatal("could not initialize instance-data fetch")
log.WithContext(ctx).WithError(err).Error("could not initialize instance-data fetch")
return OvermindInstance{}, fmt.Errorf("could not initialize instance-data fetch: %w", err)
}

req = req.WithContext(ctx)
log.WithField("instanceDataUrl", instanceDataUrl).Debug("Fetching instance-data")
res, err := otelhttp.DefaultClient.Do(req)
if err != nil {
log.WithError(err).Fatal("could not fetch instance-data")
log.WithContext(ctx).WithError(err).Error("could not fetch instance-data")
return OvermindInstance{}, fmt.Errorf("could not fetch instance-data: %w", err)
}

if res.StatusCode != 200 {
log.WithField("status-code", res.StatusCode).Fatal("instance-data fetch returned non-200 status")
log.WithContext(ctx).WithField("status-code", res.StatusCode).Error("instance-data fetch returned non-200 status")
return OvermindInstance{}, fmt.Errorf("instance-data fetch returned non-200 status: %v", res.StatusCode)
}

defer res.Body.Close()
data := instanceData{}
err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
log.WithError(err).Fatal("could not parse instance-data")
log.WithContext(ctx).WithError(err).Error("could not parse instance-data")
return OvermindInstance{}, fmt.Errorf("could not parse instance-data: %w", err)
}

instance.ApiUrl, err = url.Parse(data.Api)
if err != nil {
return instance, fmt.Errorf("invalid api_url value '%v' in instance-data, error: %w", data.Api, err)
return OvermindInstance{}, fmt.Errorf("invalid api_url value '%v' in instance-data, error: %w", data.Api, err)
}
instance.NatsUrl, err = url.Parse(data.Nats)
if err != nil {
return instance, fmt.Errorf("invalid nats_url value '%v' in instance-data, error: %w", data.Nats, err)
return OvermindInstance{}, fmt.Errorf("invalid nats_url value '%v' in instance-data, error: %w", data.Nats, err)
}

instance.Audience = data.Aud
Expand Down Expand Up @@ -217,7 +221,7 @@ func (m authenticateModel) View() string {
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:
%v
%v
Then enter the code:
Expand Down
10 changes: 7 additions & 3 deletions cmd/tea_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

// waitForCancellation returns a tea.Cmd that will wait for SIGINT and SIGTERM and run the provided cancel on receipt.
Expand Down Expand Up @@ -55,9 +56,12 @@ type taskModel struct {

func NewTaskModel(title string) taskModel {
return taskModel{
status: taskStatusPending,
title: title,
spinner: spinner.New(spinner.WithSpinner(spinner.Dot)),
status: taskStatusPending,
title: title,
spinner: spinner.New(
spinner.WithSpinner(spinner.Pulse),
spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(ColorPalette.Light.BgMain))),
),
}
}

Expand Down
26 changes: 13 additions & 13 deletions cmd/tea_ensuretoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ func (m ensureTokenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.title = "Manual device authorization."
beginAuthMessage := `# Authenticate with a browser
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:
%v
%v
Then enter the code:
Then enter the code:
%v
`
%v
`
m.deviceMessage = markdownToString(fmt.Sprintf(beginAuthMessage, msg.deviceCode.VerificationURI, msg.deviceCode.UserCode))
return m, m.awaitTokenCmd
case waitingForAuthorizationMsg:
Expand All @@ -113,15 +113,15 @@ func (m ensureTokenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.title = "Waiting for device authorization, check your browser."
beginAuthMessage := `# Authenticate with a browser
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:
%v
%v
Then enter the code:
Then enter the code:
%v
`
%v
`
m.deviceMessage = markdownToString(fmt.Sprintf(beginAuthMessage, msg.deviceCode.VerificationURI, msg.deviceCode.UserCode))
return m, m.awaitTokenCmd
case tokenLoadedMsg:
Expand Down Expand Up @@ -159,7 +159,7 @@ func (m ensureTokenModel) View() string {
if len(m.errors) > 0 {
view += fmt.Sprintf("\n%v\n", strings.Join(m.errors, "\n"))
}
if m.deviceMessage != "" {
if m.deviceMessage != "" && !(m.status == taskStatusDone || m.status == taskStatusError) {
view += fmt.Sprintf("\n%v\n", m.deviceMessage)
}
return view
Expand Down
26 changes: 15 additions & 11 deletions cmd/tea_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

tea "github.com/charmbracelet/bubbletea"
log "github.com/sirupsen/logrus"
)

type snapshotModel struct {
Expand All @@ -13,16 +14,6 @@ type snapshotModel struct {
edges uint32
}

// utility interface to remove the need for a type assertion
type connectResultStream interface {
// Receive advances the stream to the next message, which will then be
// available through the Msg method. It returns false when the stream stops,
// either by reaching the end or by encountering an unexpected error. After
// Receive returns false, the Err method will return any unexpected error
// encountered.
Receive() bool
}

type startSnapshotMsg struct {
newState string
}
Expand Down Expand Up @@ -54,11 +45,24 @@ func (m snapshotModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.items = msg.items
m.edges = msg.edges
}
log.Debugf("updated %+v => %v", msg, m)
return m, nil
}

func (m snapshotModel) View() string {
// TODO: add spinner and/or progressbar; complication: we do not have a
// expected number of items/edges to count towards for the progressbar
return fmt.Sprintf("%s: %d items, %d edges", m.state, m.items, m.edges)
if m.items == 0 && m.edges == 0 {
return fmt.Sprintf("%s - %s", m.title, m.state)
} else if m.items == 1 && m.edges == 0 {
return fmt.Sprintf("%s - %s: 1 item", m.title, m.state)
} else if m.items == 1 && m.edges == 1 {
return fmt.Sprintf("%s - %s: 1 item, 1 edge", m.title, m.state)
} else if m.items > 1 && m.edges == 0 {
return fmt.Sprintf("%s - %s: %d items", m.title, m.state, m.items)
} else if m.items > 1 && m.edges == 1 {
return fmt.Sprintf("%s - %s: %d items, 1 edge", m.title, m.state, m.items)
} else {
return fmt.Sprintf("%s - %s: %d items, %d edges", m.title, m.state, m.items, m.edges)
}
}
44 changes: 40 additions & 4 deletions cmd/tea_terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/overmindtech/sdp-go"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)

Expand Down Expand Up @@ -46,11 +47,14 @@ func (m cmdModel) Init() tea.Cmd {
waitForCancellation(m.ctx, m.cancel),
m.tasks["00_oi"].Init(),
m.tasks["01_token"].Init(),
m.tasks["02_config"].Init(),
m.cmd.Init(),
)
}

func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Debugf("cmdModel: Update %T received %+v", msg, msg)

batch := []tea.Cmd{}

// update the main command
Expand All @@ -75,23 +79,55 @@ func (m cmdModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.String() == "ctrl+c" {
return m, tea.Quit
}

case fatalError:
log.WithError(msg.err).WithField("msg.id", msg.id).Debug("cmdModel: fatalError received")
if msg.id == 0 {
m.fatalError = msg.err.Error()
}
return m, tea.Quit
// return m, nil
skipView(m.View())
return m, tea.Sequence(
tea.Batch(batch...),
tea.Quit,
)

case instanceLoadedMsg:
m.oi = msg.instance
// skip irrelevant status messages
// delete(m.tasks, "00_oi")

case tokenAvailableMsg:
return m.tokenChecks(msg.token)
tm, cmd := m.tokenChecks(msg.token)
batch = append(batch, cmd)
return tm, tea.Batch(batch...)

case tfPlanFinishedMsg, tfApplyFinishedMsg:
// bump screen after terraform ran
skipView(m.View())
}

return m, tea.Batch(batch...)
}

// skipView scrolls the terminal contents up after ExecCommand() to avoid
// overwriting the output from terraform when rendering the next View(). this
// has to be used here in the cmdModel to catch the entire View() output.
//
// NOTE: this is quite brittle and _requires_ that the View() after terraform
// returned is at least many lines as the view before ExecCommand(), otherwise
// the difference will get eaten by bubbletea on re-rendering.
//
// TODO: make this hack less ugly
func skipView(view string) {
lines := strings.Split(view, "\n")
for range lines {
fmt.Println()
}

// log.Debugf("printed %v lines:", len(lines))
// log.Debug(lines)
}

func (m cmdModel) tokenChecks(token *oauth2.Token) (cmdModel, tea.Cmd) {
// Check that we actually got the claims we asked for. If you don't have
// permission auth0 will just not assign those scopes rather than fail
Expand Down Expand Up @@ -140,7 +176,7 @@ func (m cmdModel) View() string {
}
tasks = append(tasks, m.cmd.View())
if m.fatalError != "" {
tasks = append(tasks, fmt.Sprintf("Fatal Error: %v", m.fatalError))
tasks = append(tasks, markdownToString(fmt.Sprintf("> Fatal Error: %v\n", m.fatalError)))
}
return strings.Join(tasks, "\n")
}
23 changes: 12 additions & 11 deletions cmd/terraform_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,14 @@ type runTfApplyMsg struct{}
type tfApplyFinishedMsg struct{}

func NewTfApplyModel(args []string) tea.Model {
// TODO: this is the real command
// args = append([]string{"apply"}, args...)
// // plan file needs to go last
// args = append(args, "overmind.plan")
args = append([]string{"apply"}, args...)
// plan file needs to go last
args = append(args, "overmind.plan")

// TODO: remove this test setup
args = append([]string{"plan"}, args...)
// -out needs to go last to override whatever the user specified on the command line
args = append(args, "-out", "overmind.plan")
// // TODO: remove this test setup
// args = append([]string{"plan"}, args...)
// // -out needs to go last to override whatever the user specified on the command line
// args = append(args, "-out", "overmind.plan")

applyHeader := `# Applying Changes
Expand All @@ -73,7 +72,7 @@ Running ` + "`" + `terraform %v` + "`\n"

processingHeader := `# Applying Changes
Processing plan from ` + "`" + `terraform %v` + "`\n"
Applying changes with ` + "`" + `terraform %v` + "`\n"
processingHeader = fmt.Sprintf(processingHeader, strings.Join(args, " "))

return tfApplyModel{
Expand Down Expand Up @@ -102,6 +101,7 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case sourcesInitialisedMsg:
m.isStarting = true
return m, tea.Batch(
m.startingChangeSnapshot.Init(),
m.startStartChangeCmd(),
m.waitForStartingActivity,
)
Expand Down Expand Up @@ -159,6 +159,7 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tfApplyFinishedMsg:
m.isEnding = true
return m, tea.Batch(
m.endingChangeSnapshot.Init(),
m.startEndChangeCmd(),
m.waitForEndingActivity,
)
Expand All @@ -169,13 +170,13 @@ func (m tfApplyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

func (m tfApplyModel) View() string {
if m.isStarting || m.runTfApply || m.isEnding {
return m.processingHeader +
return markdownToString(m.processingHeader) + "\n" +
m.startingChangeSnapshot.View() + "\n" +
m.endingChangeSnapshot.View() + "\n" +
strings.Join(m.progress, "\n") + "\n"
}

return m.applyHeader
return markdownToString(m.applyHeader) + "\n"
}

func (m tfApplyModel) startStartChangeCmd() tea.Cmd {
Expand Down
Loading

0 comments on commit e1f6dc8

Please sign in to comment.