diff --git a/cli/cmds/interactive/interactive.go b/cli/cmds/interactive/interactive.go index dbfcc79..57e3067 100644 --- a/cli/cmds/interactive/interactive.go +++ b/cli/cmds/interactive/interactive.go @@ -45,7 +45,8 @@ func NewCommand() *cli.Command { func migrate(clx *cli.Context) error { ctx := context.Background() - + cmds.Spinner.Prefix = fmt.Sprintf("initiating source [%s] and target [%s] clusters objects.. ", source, target) + cmds.Spinner.Start() restConfig, err := clientcmd.BuildConfigFromFlags("", cmds.Kubeconfig) if err != nil { return err @@ -100,8 +101,6 @@ func migrate(clx *cli.Context) error { Client: tcClient, } - cmds.Spinner.Prefix = fmt.Sprintf("initiating source [%s] and target [%s] clusters objects.. ", sc.Obj.Spec.DisplayName, tc.Obj.Spec.DisplayName) - cmds.Spinner.Start() if err := sc.Populate(ctx, cl); err != nil { return err } diff --git a/cli/cmds/migrate/migrate.go b/cli/cmds/migrate/migrate.go index 1eb6d5a..38dfd76 100644 --- a/cli/cmds/migrate/migrate.go +++ b/cli/cmds/migrate/migrate.go @@ -6,6 +6,7 @@ import ( "galal-hussein/cattle-drive/cli/cmds" "galal-hussein/cattle-drive/pkg/client" "galal-hussein/cattle-drive/pkg/cluster" + "os" v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" "github.com/sirupsen/logrus" @@ -44,7 +45,8 @@ func NewCommand() *cli.Command { func migrate(clx *cli.Context) error { ctx := context.Background() - + cmds.Spinner.Prefix = fmt.Sprintf("initiating source [%s] and target [%s] clusters objects.. ", source, target) + cmds.Spinner.Start() restConfig, err := clientcmd.BuildConfigFromFlags("", cmds.Kubeconfig) if err != nil { return err @@ -98,8 +100,6 @@ func migrate(clx *cli.Context) error { Obj: targetCluster, Client: tcClient, } - cmds.Spinner.Prefix = fmt.Sprintf("initiating source [%s] and target [%s] clusters objects.. ", sc.Obj.Spec.DisplayName, tc.Obj.Spec.DisplayName) - cmds.Spinner.Start() if err := sc.Populate(ctx, cl); err != nil { return err } @@ -110,5 +110,5 @@ func migrate(clx *cli.Context) error { return err } cmds.Spinner.Stop() - return sc.Migrate(ctx, cl, tc) + return sc.Migrate(ctx, cl, tc, os.Stdout) } diff --git a/cli/cmds/status/status.go b/cli/cmds/status/status.go index 4fea869..0112b4b 100644 --- a/cli/cmds/status/status.go +++ b/cli/cmds/status/status.go @@ -44,7 +44,8 @@ func NewCommand() *cli.Command { func status(clx *cli.Context) error { ctx := context.Background() - + cmds.Spinner.Prefix = fmt.Sprintf("initiating source [%s] and target [%s] clusters objects.. ", source, target) + cmds.Spinner.Start() restConfig, err := clientcmd.BuildConfigFromFlags("", cmds.Kubeconfig) if err != nil { return err @@ -98,8 +99,6 @@ func status(clx *cli.Context) error { Obj: targetCluster, Client: tcClient, } - cmds.Spinner.Prefix = fmt.Sprintf("initiating source [%s] and target [%s] clusters objects.. ", sc.Obj.Spec.DisplayName, tc.Obj.Spec.DisplayName) - cmds.Spinner.Start() if err := sc.Populate(ctx, cl); err != nil { return err } diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 31f91b7..f84df54 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "galal-hussein/cattle-drive/pkg/client" + "io" "reflect" v1catalog "github.com/rancher/rancher/pkg/apis/catalog.cattle.io/v1" @@ -230,60 +231,60 @@ func (c *Cluster) Status(ctx context.Context, client *client.Clients) error { return nil } -func (c *Cluster) Migrate(ctx context.Context, client *client.Clients, tc *Cluster) error { - fmt.Printf("Migrating Objects from cluster [%s] to cluster [%s]:\n", c.Obj.Spec.DisplayName, tc.Obj.Spec.DisplayName) +func (c *Cluster) Migrate(ctx context.Context, client *client.Clients, tc *Cluster, w io.Writer) error { + fmt.Fprintf(w, "Migrating Objects from cluster [%s] to cluster [%s]:\n", c.Obj.Spec.DisplayName, tc.Obj.Spec.DisplayName) for _, p := range c.ToMigrate.Projects { if !p.Migrated { - fmt.Printf("- migrating Project [%s]... ", p.Name) + fmt.Fprintf(w, "- migrating Project [%s]... ", p.Name) p.Mutate(tc) if err := client.Projects.Create(ctx, tc.Obj.Name, p.Obj, nil, v1.CreateOptions{}); err != nil { return err } - fmt.Printf("Done.\n") + fmt.Fprintf(w, "Done.\n") } for _, prtb := range p.PRTBs { if !prtb.Migrated { - fmt.Printf(" - migrating PRTB [%s]... ", prtb.Name) + fmt.Fprintf(w, " - migrating PRTB [%s]... ", prtb.Name) // if project is already migrated then we find out the new project name in the mgirated cluster prtb.Mutate(tc.Obj.Name, prtb.ProjectName) if err := client.ProjectRoleTemplateBindings.Create(ctx, prtb.ProjectName, prtb.Obj, nil, v1.CreateOptions{}); err != nil { return err } - fmt.Printf("Done.\n") + fmt.Fprintf(w, "Done.\n") } } for _, ns := range p.Namespaces { if !ns.Migrated { - fmt.Printf(" - migrating Namespace [%s]... ", ns.Name) + fmt.Fprintf(w, " - migrating Namespace [%s]... ", ns.Name) ns.Mutate(tc.Obj.Name, ns.ProjectName) if _, err := tc.Client.Namespace.Create(ns.Obj); err != nil { return err } - fmt.Printf("Done.\n") + fmt.Fprintf(w, "Done.\n") } } } for _, crtb := range c.ToMigrate.CRTBs { if !crtb.Migrated { - fmt.Printf("- migrating CRTB [%s]... ", crtb.Name) + fmt.Fprintf(w, "- migrating CRTB [%s]... ", crtb.Name) crtb.Mutate(tc) if err := client.ClusterRoleTemplateBindings.Create(ctx, tc.Obj.Name, crtb.Obj, nil, v1.CreateOptions{}); err != nil { return err } - fmt.Printf("Done.\n") + fmt.Fprintf(w, "Done.\n") } } // catalog repos for _, repo := range c.ToMigrate.ClusterRepos { if !repo.Migrated { - fmt.Printf("- migrating catalog repo [%s]... ", repo.Name) + fmt.Fprintf(w, "- migrating catalog repo [%s]... ", repo.Name) repo.Mutate() if err := tc.Client.ClusterRepos.Create(ctx, tc.Obj.Name, repo.Obj, nil, v1.CreateOptions{}); err != nil { return err } - fmt.Printf("Done.\n") + fmt.Fprintf(w, "Done.\n") } } diff --git a/pkg/cluster/tui/cluster.go b/pkg/cluster/tui/cluster.go index d9bdc00..8b56677 100644 --- a/pkg/cluster/tui/cluster.go +++ b/pkg/cluster/tui/cluster.go @@ -1,10 +1,15 @@ package tui import ( + "bytes" + "context" "galal-hussein/cattle-drive/pkg/cluster/tui/constants" + "strings" + "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" ) @@ -17,9 +22,11 @@ const ( ) type Model struct { - mode mode - list list.Model - quitting bool + mode mode + migratingAll bool + list list.Model + progress progress.Model + quitting bool } type item struct { @@ -43,19 +50,22 @@ func (m Model) Init() tea.Cmd { return nil } -func InitCluster() (tea.Model, tea.Cmd) { +func InitCluster(msg tea.Msg) (tea.Model, tea.Cmd) { + prog := progress.New(progress.WithSolidFill("#04B575")) items := newClusterList() delegate := newItemDelegate(delegateKeys) clusterList := list.New(items, delegate, 8, 8) clusterList.Styles.Title = constants.TitleStyle - m := Model{mode: nav, list: clusterList} + m := Model{mode: nav, list: clusterList, progress: prog} if constants.WindowSize.Height != 0 { top, right, bottom, left := constants.DocStyle.GetMargin() m.list.SetSize(constants.WindowSize.Width-left-right, constants.WindowSize.Height-top-bottom-1) } m.list.Title = "Cluster " + constants.SC.Obj.Spec.DisplayName + " migration" - + if msg != nil { + return m, func() tea.Msg { return msg } + } return m, func() tea.Msg { return errMsg{nil} } } @@ -73,6 +83,25 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { + case tickMsg: + m.migratingAll = true + for { + select { + case <-time.After(time.Millisecond * 500): + if m.progress.Percent() == 1.0 { + cmd := m.progress.SetPercent(0) + return m, tea.Batch(tickCmd(), cmd) + } + cmd := m.progress.IncrPercent(0.25) + return m, tea.Batch(tickCmd(), cmd) + case <-constants.Migratedch: + return InitCluster(nil) + } + } + case progress.FrameMsg: + progressModel, cmd := m.progress.Update(msg) + m.progress = progressModel.(progress.Model) + return m, cmd case tea.WindowSizeMsg: constants.WindowSize = msg top, right, bottom, left := constants.DocStyle.GetMargin() @@ -85,7 +114,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, delegateKeys.Enter): entry := InitObjects(m.list.SelectedItem().(item)) return entry.Update(constants.WindowSize) - + case key.Matches(msg, delegateKeys.MigrateAll): + m.mode = migrate + go m.migrateCluster(context.Background()) + return InitCluster(tickMsg{}) default: m.list, cmd = m.list.Update(msg) } @@ -100,5 +132,20 @@ func (m Model) View() string { if m.quitting { return "" } + if m.migratingAll { + pad := strings.Repeat(" ", 2) + return "\n\n Migrating all objects.. please wait" + pad + m.progress.View() + "\n\n" + pad + } return constants.DocStyle.Render(m.list.View() + "\n") } + +func (m *Model) migrateCluster(ctx context.Context) { + var buf bytes.Buffer + if err := constants.SC.Migrate(ctx, constants.Lclient, constants.TC, &buf); err != nil { + m.Update(tea.Quit()) + } + if err := updateClusters(ctx); err != nil { + m.Update(tea.Quit()) + } + constants.Migratedch <- true +} diff --git a/pkg/cluster/tui/constants/constants.go b/pkg/cluster/tui/constants/constants.go index aaa66b3..a9f30ee 100644 --- a/pkg/cluster/tui/constants/constants.go +++ b/pkg/cluster/tui/constants/constants.go @@ -44,6 +44,7 @@ var ( Lclient *client.Clients // WindowSize store the size of the terminal window WindowSize tea.WindowSizeMsg + Migratedch = make(chan bool) ) /* STYLING */ diff --git a/pkg/cluster/tui/delegate.go b/pkg/cluster/tui/delegate.go index f3f8894..988f0e8 100644 --- a/pkg/cluster/tui/delegate.go +++ b/pkg/cluster/tui/delegate.go @@ -31,7 +31,7 @@ func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { return nil } - help := []key.Binding{keys.Enter, keys.Migrate, keys.Back} + help := []key.Binding{keys.Enter, keys.Migrate, keys.MigrateAll, keys.Back} d.ShortHelpFunc = func() []key.Binding { return help @@ -45,16 +45,18 @@ func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { } type delegateKeyMap struct { - Migrate key.Binding - Back key.Binding - Enter key.Binding - Quit key.Binding + MigrateAll key.Binding + Migrate key.Binding + Back key.Binding + Enter key.Binding + Quit key.Binding } // Additional short help entries. This satisfies the help.KeyMap interface and // is entirely optional. func (d delegateKeyMap) ShortHelp() []key.Binding { return []key.Binding{ + d.MigrateAll, d.Migrate, d.Back, d.Enter, @@ -67,6 +69,7 @@ func (d delegateKeyMap) ShortHelp() []key.Binding { func (d delegateKeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ { + d.MigrateAll, d.Migrate, d.Back, d.Enter, @@ -85,6 +88,10 @@ func newDelegateKeyMap() *delegateKeyMap { key.WithKeys("m"), key.WithHelp("m", "migrate"), ), + MigrateAll: key.NewBinding( + key.WithKeys("a"), + key.WithHelp("a", "migrate all"), + ), Back: key.NewBinding( key.WithKeys("esc"), key.WithHelp("esc", "main menu"), diff --git a/pkg/cluster/tui/objects.go b/pkg/cluster/tui/objects.go index c413d58..2a5a378 100644 --- a/pkg/cluster/tui/objects.go +++ b/pkg/cluster/tui/objects.go @@ -23,7 +23,6 @@ type Objects struct { mode mode list list.Model quitting bool - percent float64 progress progress.Model activeObject string } @@ -91,7 +90,9 @@ func InitObjects(i item) *Objects { items = append(items, i) } } - delegate := newItemDelegate(delegateKeys) + delegateObjKeys := *delegateKeys + delegateObjKeys.MigrateAll = key.NewBinding() + delegate := newItemDelegate(&delegateObjKeys) objList := list.New(items, delegate, 8, 8) objList.Styles.Title = constants.TitleStyle m.list = objList @@ -113,19 +114,23 @@ func (m Objects) Update(msg tea.Msg) (tea.Model, tea.Cmd) { top, right, bottom, left := constants.DocStyle.GetMargin() m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom-1) case tickMsg: - m.percent += 0.1 - if m.percent > 1.0 || m.mode == migrated { - m.percent = 1.0 - return InitCluster() + m.mode = migrate + cmd := m.progress.IncrPercent(0.25) + if m.progress.Percent() == 1.0 || m.mode == migrated { + return InitCluster(nil) } - return m, tickCmd() + return m, tea.Batch(tickCmd(), cmd) + case progress.FrameMsg: + progressModel, cmd := m.progress.Update(msg) + m.progress = progressModel.(progress.Model) + return m, cmd case tea.KeyMsg: switch { case key.Matches(msg, delegateKeys.Quit): m.quitting = true return m, tea.Quit case key.Matches(msg, delegateKeys.Back): - return InitCluster() + return InitCluster(nil) case key.Matches(msg, delegateKeys.Enter): if m.list.SelectedItem() == nil { return m, tea.Batch(cmds...) @@ -162,7 +167,7 @@ func (m Objects) View() string { } if m.mode == migrate { pad := strings.Repeat(" ", 2) - return "\n\n Waiting for object [" + m.activeObject + "] to be migrated\n\n" + pad + m.progress.ViewAs(m.percent) + "\n\n" + pad + return "\n\n Waiting for object [" + m.activeObject + "] to be migrated\n\n" + pad + m.progress.View() + "\n\n" + pad } return constants.DocStyle.Render(m.list.View() + "\n") } diff --git a/pkg/cluster/tui/tui.go b/pkg/cluster/tui/tui.go index 916df4f..c65571f 100644 --- a/pkg/cluster/tui/tui.go +++ b/pkg/cluster/tui/tui.go @@ -27,7 +27,7 @@ func StartTea(sc, tc *cluster.Cluster, client *client.Clients) error { constants.TC = tc constants.Lclient = client - m, _ := InitCluster() + m, _ := InitCluster(nil) constants.P = tea.NewProgram(m, tea.WithAltScreen()) if _, err := constants.P.Run(); err != nil { return err