Skip to content

Commit

Permalink
WIP: progress bar in box working
Browse files Browse the repository at this point in the history
  • Loading branch information
UncleGedd committed Feb 2, 2024
1 parent d69f7ad commit a8b0759
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 12 deletions.
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ replace github.com/derailed/k9s => github.com/derailed/k9s v0.31.7
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/defenseunicorns/zarf v0.32.2
github.com/goccy/go-yaml v1.11.3
github.com/mholt/archiver/v3 v3.5.1
Expand Down Expand Up @@ -139,10 +142,7 @@ require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/charmbracelet/bubbles v0.16.1 // indirect
github.com/charmbracelet/bubbletea v0.25.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
Expand Down
17 changes: 14 additions & 3 deletions src/cmd/uds.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@
package cmd

import (
"bytes"
"os"
"path/filepath"
"strings"

"github.com/AlecAivazis/survey/v2"
tea "github.com/charmbracelet/bubbletea"
"github.com/defenseunicorns/uds-cli/src/config"
"github.com/defenseunicorns/uds-cli/src/config/lang"
"github.com/defenseunicorns/uds-cli/src/pkg/bundle"
"github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui"
zarfConfig "github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/pkg/message"
zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils"
zarfTypes "github.com/defenseunicorns/zarf/src/types"
goyaml "github.com/goccy/go-yaml"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -83,9 +87,16 @@ var deployCmd = &cobra.Command{
bndlClient := bundle.NewOrDie(&bundleCfg)
defer bndlClient.ClearPaths()

if err := bndlClient.Deploy(); err != nil {
bndlClient.ClearPaths()
message.Fatalf(err, "Failed to deploy bundle: %s", err.Error())
// disable pterm output (for now) and set default output to buffer
pterm.DisableOutput()
var ptermBuffer bytes.Buffer
pterm.SetDefaultOutput(&ptermBuffer)

// start up bubbletea
m := tui.InitModel("", bndlClient, &ptermBuffer, tui.DeployOp)
tui.Program = tea.NewProgram(&m, tea.WithMouseAllMotion())
if _, err := tui.Program.Run(); err != nil {
panic(err)
}
},
}
Expand Down
1 change: 1 addition & 0 deletions src/pkg/bundle/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
)

// Bundler handles bundler operations
// todo: rename this mofo, bundler vs bundle is confusing
type Bundler struct {
// cfg is the Bundler's configuration options
cfg *types.BundlerConfig
Expand Down
20 changes: 16 additions & 4 deletions src/pkg/bundle/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package bundle

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand All @@ -14,6 +15,7 @@ import (

"github.com/AlecAivazis/survey/v2"
"github.com/defenseunicorns/uds-cli/src/config"
"github.com/defenseunicorns/uds-cli/src/pkg/bundle/tui"
"github.com/defenseunicorns/uds-cli/src/pkg/sources"
"github.com/defenseunicorns/uds-cli/src/types"
zarfConfig "github.com/defenseunicorns/zarf/src/config"
Expand All @@ -40,7 +42,7 @@ type ZarfOverrideMap map[string]map[string]map[string]interface{}
// : : load the package into a fresh temp dir
// : : validate the sig (if present)
// : : deploy the package
func (b *Bundler) Deploy() error {
func (b *Bundler) Deploy(ptermBuf *bytes.Buffer) error {
ctx := context.TODO()

pterm.Println()
Expand Down Expand Up @@ -115,13 +117,13 @@ func (b *Bundler) Deploy() error {
if len(userSpecifiedPackages) != len(packagesToDeploy) {
return fmt.Errorf("invalid zarf packages specified by --packages")
}
return deployPackages(packagesToDeploy, resume, b, zarfPackageNameMap)
return deployPackages(packagesToDeploy, resume, b, ptermBuf, zarfPackageNameMap)
}

return deployPackages(b.bundle.Packages, resume, b, zarfPackageNameMap)
return deployPackages(b.bundle.Packages, resume, b, ptermBuf, zarfPackageNameMap)
}

func deployPackages(packages []types.Package, resume bool, b *Bundler, zarfPackageNameMap map[string]string) error {
func deployPackages(packages []types.Package, resume bool, b *Bundler, ptermBuf *bytes.Buffer, zarfPackageNameMap map[string]string) error {
// map of Zarf pkgs and their vars
bundleExportedVars := make(map[string]map[string]string)

Expand Down Expand Up @@ -194,10 +196,20 @@ func deployPackages(packages []types.Package, resume bool, b *Bundler, zarfPacka
if err != nil {
return err
}

// enable output to start filling pterm buffer
pterm.EnableOutput()

// bubbletea recommends calling the Program directly; calling model.Update() doesn't work
// https://github.com/charmbracelet/bubbletea/discussions/374
tui.Program.Send(fmt.Sprintf("package:%s", pkg.Name))
if err := pkgClient.Deploy(); err != nil {
return err
}

// need to reset the output buffer because Deploy() sets it to stderr
pterm.SetDefaultOutput(ptermBuf)

// save exported vars
pkgExportedVars := make(map[string]string)
for _, exp := range pkg.Exports {
Expand Down
235 changes: 235 additions & 0 deletions src/pkg/bundle/tui/tui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package tui

Check warning on line 1 in src/pkg/bundle/tui/tui.go

View workflow job for this annotation

GitHub Actions / validate

should have a package comment

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"

"github.com/charmbracelet/bubbles/progress"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/pkg/k8s"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/types"
"github.com/pterm/pterm"
)

type tickMsg time.Time
type operation string

const (
DeployOp operation = "deploy"

Check warning on line 27 in src/pkg/bundle/tui/tui.go

View workflow job for this annotation

GitHub Actions / validate

exported const DeployOp should have comment (or a comment on this block) or be unexported
tick operation = "tick"
)

var (
Program *tea.Program

Check warning on line 32 in src/pkg/bundle/tui/tui.go

View workflow job for this annotation

GitHub Actions / validate

exported var Program should have comment or be unexported
)

func InitModel(content string, client bndlClientShim, buf *bytes.Buffer, op operation) model {

Check warning on line 35 in src/pkg/bundle/tui/tui.go

View workflow job for this annotation

GitHub Actions / validate

exported func InitModel returns unexported type tui.model, which can be annoying to use

Check warning on line 35 in src/pkg/bundle/tui/tui.go

View workflow job for this annotation

GitHub Actions / validate

exported function InitModel should have comment or be unexported
return model{
content: content,
bndlClient: client,
packageOutputBuffer: buf,
op: op,
quitChan: make(chan int),
componentChan: make(chan int),
progress: progress.New(progress.WithDefaultGradient()),
currentPkg: "",
currentComponent: 1,
}
}

// private interface to decouple tui pkg from bundle pkg
type bndlClientShim interface {
Deploy(*bytes.Buffer) error
ClearPaths()
}

type model struct {
content string
ready bool
packageOutputBuffer *bytes.Buffer
bndlClient bndlClientShim
op operation
quitChan chan int
progress progress.Model
currentPkg string
numComponents int
componentChan chan int
currentComponent int
}

func (m model) Init() tea.Cmd {
return func() tea.Msg {
return m.op
}
}

// allows us to get way more Zarf output
// adopted from:
// https://stackoverflow.com/questions/74375547/how-to-deal-with-log-output-which-contains-progress-bar
func cleanFlushInfo(bytesBuffer *bytes.Buffer) string {
scanner := bufio.NewScanner(bytesBuffer)
finalString := ""

for scanner.Scan() {
line := scanner.Text()
chunks := strings.Split(line, "\r")
lastChunk := chunks[len(chunks)-1] // fetch the last update of the line
finalString += lastChunk + "\n"
}
return finalString
}

func GetDeployedPackage(packageName string) (deployedPackage *types.DeployedPackage) {

Check warning on line 91 in src/pkg/bundle/tui/tui.go

View workflow job for this annotation

GitHub Actions / validate

exported function GetDeployedPackage should have comment or be unexported
// Get the secret that describes the deployed package
k8sClient, _ := k8s.New(message.Debugf, k8s.Labels{config.ZarfManagedByLabel: "zarf"})
secret, err := k8sClient.GetSecret("zarf", config.ZarfPackagePrefix+packageName)
if err != nil {
return deployedPackage
}

err = json.Unmarshal(secret.Data["data"], &deployedPackage)
if err != nil {
panic(0)
}
return deployedPackage
}

func finalPause() tea.Cmd {
return tea.Tick(time.Millisecond*750, func(_ time.Time) tea.Msg {
return nil
})
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmds []tea.Cmd
)
select {
case <-m.quitChan:
return m, tea.Sequence(tea.Quit)
default:
switch msg := msg.(type) {
case progress.FrameMsg:
progressModel, cmd := m.progress.Update(msg)
m.progress = progressModel.(progress.Model)
return m, cmd
case tickMsg:
var cmd tea.Cmd
if m.numComponents > 0 {
deployedPkg := GetDeployedPackage(m.currentPkg)
if deployedPkg != nil {
if m.currentComponent == m.numComponents {
cmd = m.progress.SetPercent(100.0)
} else {
m.currentComponent++
cmd = m.progress.IncrPercent(float64(m.currentComponent) / float64(m.numComponents))
}
}
}
return m, tea.Sequence(cmd, tickCmd())
case operation:
m.ready = true
m.packageOutputBuffer.Reset()

switch msg {
case DeployOp:
// run Deploy concurrently so we can update the TUI while it runs
go func() {
// todo: don't actually put the buffer in the call to Deploy()
if err := m.bndlClient.Deploy(m.packageOutputBuffer); err != nil {
// use existing Zarf pterm things for errors
pterm.EnableOutput()
pterm.SetDefaultOutput(os.Stderr)
m.bndlClient.ClearPaths()
message.Fatalf(err, "Failed to deploy bundle: %s", err.Error())
m.quitChan <- 1
}
m.quitChan <- 1
}()
// use a ticker to update the TUI while the deploy runs
return m, tickCmd()
}
case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
return m, tea.Quit
}
case string:
if strings.Split(msg, ":")[0] == "package" {
pkgName := strings.Split(msg, ":")[1]
m.currentPkg = pkgName
} else if strings.Split(msg, ":")[0] == "numComponents" {
if totalComponents, err := strconv.Atoi(strings.Split(msg, ":")[1]); err == nil {
m.numComponents = totalComponents // else return err
}

}
}
}

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

func (m model) View() string {
if !m.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s", m.boxView())
}

func (m model) boxView() string {
width := 100
question := lipgloss.NewStyle().
Width(50).
Align(lipgloss.Left).
Padding(0, 3).
Render(fmt.Sprintf("📦 Deploying: %s", m.currentPkg))

progressBar := lipgloss.NewStyle().
Width(50).
Align(lipgloss.Left).
Padding(0, 3).
MarginTop(1).
Render(m.progress.View())

ui := lipgloss.JoinVertical(lipgloss.Center, question, progressBar)

boxStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#874BFD")).
Padding(1, 0).
BorderTop(true).
BorderLeft(true).
BorderRight(true).
BorderBottom(true)
subtle := lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}

box := lipgloss.Place(width, 9,
lipgloss.Left, lipgloss.Top,
boxStyle.Render(ui),
lipgloss.WithWhitespaceForeground(subtle),
)

return box
}

func tickCmd() tea.Cmd {
return tea.Tick(time.Millisecond, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}

func max(a, b int) int {
if a > b {
return a
}
return b
}
Loading

0 comments on commit a8b0759

Please sign in to comment.