Skip to content

Commit

Permalink
feat: install providers in parallel
Browse files Browse the repository at this point in the history
  • Loading branch information
Madh93 committed May 19, 2023
1 parent 0f92527 commit 92b1f3b
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 174 deletions.
3 changes: 3 additions & 0 deletions cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"runtime"

"github.com/Madh93/tpm/cmd/tui"
"github.com/Madh93/tpm/internal/mathutils"
"github.com/Madh93/tpm/internal/terraform"
"github.com/Madh93/tpm/internal/tpm"
"github.com/spf13/cobra"
Expand All @@ -28,6 +29,7 @@ var installCmd = &cobra.Command{
},
Run: func(cmd *cobra.Command, args []string) {
viper.Set("force", getBoolFlag(cmd, "force"))
viper.Set("jobs", mathutils.Max(1, getIntFlag(cmd, "jobs")))
var providers []*terraform.Provider
var err error

Expand Down Expand Up @@ -63,6 +65,7 @@ func init() {
// Local Flags
installCmd.Flags().Bool("force", false, "forces the installation of the provider even if it already exists")
installCmd.Flags().StringP("from-file", "f", "", "installs providers defined in a 'providers.yml' file")
installCmd.Flags().IntP("jobs", "j", 4, "specifies maximum number of concurrent providers to install")
installCmd.Flags().StringSliceP("os", "o", []string{runtime.GOOS}, "terraform provider operating system")
installCmd.Flags().StringSliceP("arch", "a", []string{runtime.GOARCH}, "terraform provider architecture")
}
92 changes: 11 additions & 81 deletions cmd/tui/installer.go
Original file line number Diff line number Diff line change
@@ -1,99 +1,29 @@
package tui

import (
"fmt"

"github.com/Madh93/tpm/internal/terraform"
"github.com/Madh93/tpm/internal/tpm"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/viper"
)

type installer struct {
spinner spinner.Model
providers []*terraform.Provider
index int
err error
}
type InstallRunner struct{}

func NewInstallerModel(providers []*terraform.Provider) installer {
return installer{
spinner: DefaultSpinner,
providers: providers,
index: 0,
func (r *InstallRunner) RunCmd(job ProviderJob) tea.Cmd {
return func() tea.Msg {
job.err = tpm.Install(job.provider, viper.GetBool("force"))
return processedJobMsg(job)
}
}

func (r InstallRunner) String() string {
return "Installing"
}

func RunInstaller(providers []*terraform.Provider) (err error) {
if _, err = tea.NewProgram(NewInstallerModel(providers)).Run(); err != nil {
model := NewModel(&InstallRunner{}, providers)
if _, err = tea.NewProgram(model).Run(); err != nil {
return
}
return nil
}

func (i installer) Init() tea.Cmd {
return tea.Batch(
installProvider(i.providers[i.index]),
i.spinner.Tick,
)
}

func (i installer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {

case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return i, tea.Quit
}

case spinner.TickMsg:
var cmd tea.Cmd
i.spinner, cmd = i.spinner.Update(msg)
return i, cmd

case providerMsg:
if i.index >= len(i.providers)-1 {
return i, tea.Sequence(
tea.Printf("%s %s", CheckMark, i.providers[i.index]),
tea.Quit,
)
}
i.index++
return i, tea.Batch(
tea.Printf("%s %s", CheckMark, i.providers[i.index-1]),
installProvider(i.providers[i.index]),
)

case errMsg:
i.err = msg
if i.index >= len(i.providers)-1 {
return i, tea.Sequence(
tea.Printf("%s %s Reason: %s", CrossMark, i.providers[i.index], i.err),
tea.Quit,
)
}
i.index++
return i, tea.Batch(
tea.Printf("%s %s Reason: %s", CrossMark, i.providers[i.index-1], i.err),
installProvider(i.providers[i.index]),
)
}

return i, nil
}

func (i installer) View() string {
return fmt.Sprintf("%s Installing %s", i.spinner.View(), i.providers[i.index])
}

func installProvider(provider *terraform.Provider) tea.Cmd {
return func() tea.Msg {
err := tpm.Install(provider, viper.GetBool("force"))
if err != nil {
return errMsg{err}
}
return providerMsg(provider.String())
}
}
30 changes: 30 additions & 0 deletions cmd/tui/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tui

import (
"fmt"

"github.com/Madh93/tpm/internal/terraform"
tea "github.com/charmbracelet/bubbletea"
)

type ProviderJob struct {
id int
provider *terraform.Provider
done bool
err error
}

type JobRunner interface {
RunCmd(job ProviderJob) tea.Cmd
String() string
}

func NewRunner(runner string) (JobRunner, error) {
switch runner {
case "install":
return &InstallRunner{}, nil
case "uninstall":
return &UninstallRunner{}, nil
}
return nil, fmt.Errorf("unsupported '%s' job runner", runner)
}
143 changes: 140 additions & 3 deletions cmd/tui/tui.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package tui

import (
"fmt"
"time"

"github.com/Madh93/tpm/internal/mathutils"
"github.com/Madh93/tpm/internal/terraform"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/viper"
)

var (
Expand All @@ -11,8 +18,138 @@ var (
CrossMark = lipgloss.NewStyle().Foreground(lipgloss.Color("160")).SetString("⨯")
)

type providerMsg string
type processedJobMsg ProviderJob

type tearDownMsg bool

type model struct {
spinner spinner.Model
runner JobRunner
jobs []ProviderJob
maxConcurrentJobs int
doneJobs int
index int
}

func NewModel(runner JobRunner, providers []*terraform.Provider) *model {
// Setup jobs
jobs := []ProviderJob{}
for i, provider := range providers {
jobs = append(jobs, ProviderJob{id: i, provider: provider, done: false, err: nil})
}

return &model{
spinner: DefaultSpinner,
runner: runner,
jobs: jobs,
maxConcurrentJobs: viper.GetInt("jobs"),
}
}

func (m *model) Init() tea.Cmd {
// Setup spinner
cmds := []tea.Cmd{m.spinner.Tick}

// Add inital jobs
for _, job := range m.jobs[:mathutils.Min(m.maxConcurrentJobs, len(m.jobs))] {
cmds = append(cmds, m.runner.RunCmd(job))
m.index++
}

return tea.Batch(cmds...)
}

func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {

case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
}

case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd

case processedJobMsg:
m.FinishJob(msg)
output := m.GetJobOutput(msg)

// Latest job! Quitting...
if m.doneJobs >= len(m.jobs) {
return m, tea.Sequence(
tea.Println(output),
tearDownCmd(),
)
}

// Print result to output
cmds := []tea.Cmd{tea.Println(output)}

// Adding pending jobs
if m.index <= len(m.jobs) {
cmds = append(cmds, m.runner.RunCmd(m.jobs[m.index-1]))
}

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

case tearDownMsg:
for {
time.Sleep(time.Millisecond * time.Duration(100))
allDone := true
for _, job := range m.jobs {
if !job.done {
allDone = false
break
}
}

if allDone {
break
}
}
return m, tea.Quit
}

return m, nil
}

func (m model) View() string {
var view string
var currentJobs int

for _, job := range m.jobs {
// Limit 'Installing' providers to the Max Concurrent Jobs
if currentJobs >= m.maxConcurrentJobs {
break
}
if !job.done {
view += fmt.Sprintf("%s %s %s\n", m.spinner.View(), m.runner, job.provider)
currentJobs++
}
}

return view
}

func (m *model) FinishJob(job processedJobMsg) {
m.jobs[job.id].done = true
m.index++
m.doneJobs++
}

func (m model) GetJobOutput(job processedJobMsg) string {
if job.err != nil {
return fmt.Sprintf("%s %s Reason: %s", CrossMark, job.provider, job.err)
}

type errMsg struct{ err error }
return fmt.Sprintf("%s %s", CheckMark, job.provider)
}

func (e errMsg) Error() string { return e.err.Error() }
func tearDownCmd() tea.Cmd {
return func() tea.Msg {
return tearDownMsg(true)
}
}
Loading

0 comments on commit 92b1f3b

Please sign in to comment.