diff --git a/.gitignore b/.gitignore index 0100e2ed8..78afda009 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ directpv kubectl-directpv !kubectl-directpv/ vdb.xml +drives.yaml diff --git a/cmd/kubectl-directpv/discover.go b/cmd/kubectl-directpv/discover.go index 8747bbc4b..7834e3608 100644 --- a/cmd/kubectl-directpv/discover.go +++ b/cmd/kubectl-directpv/discover.go @@ -22,8 +22,10 @@ import ( "fmt" "os" "strings" + "sync" "time" + tea "github.com/charmbracelet/bubbletea" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" directpvtypes "github.com/minio/directpv/pkg/apis/directpv.min.io/types" @@ -201,9 +203,11 @@ func writeInitConfig(config InitConfig) error { return config.Write(f) } -func discoverDevices(ctx context.Context, nodes []types.Node) (devices map[directpvtypes.NodeID][]types.Device, err error) { +func discoverDevices(ctx context.Context, nodes []types.Node, teaProgram *tea.Program) (devices map[directpvtypes.NodeID][]types.Device, err error) { var nodeNames []string nodeClient := client.NodeClient() + totalNodeCount := len(nodes) + discoveryProgressMap := make(map[string]progressLog, totalNodeCount) for i := range nodes { nodeNames = append(nodeNames, nodes[i].Name) updateFunc := func() error { @@ -215,6 +219,14 @@ func discoverDevices(ctx context.Context, nodes []types.Node) (devices map[direc if _, err := nodeClient.Update(ctx, node, metav1.UpdateOptions{TypeMeta: types.NewNodeTypeMeta()}); err != nil { return err } + if teaProgram != nil { + discoveryProgressMap[node.Name] = progressLog{ + log: fmt.Sprintf("Discovering node '%v'", node.Name), + } + teaProgram.Send(progressNotification{ + progressLogs: toProgressLogs(discoveryProgressMap), + }) + } return nil } if err = retry.RetryOnConflict(retry.DefaultRetry, updateFunc); err != nil { @@ -249,6 +261,15 @@ func discoverDevices(ctx context.Context, nodes []types.Node) (devices map[direc node := event.Node if !node.Spec.Refresh { devices[directpvtypes.NodeID(node.Name)] = node.GetDevicesByNames(drivesArgs) + if teaProgram != nil { + discoveryProgressMap[node.Name] = progressLog{ + log: fmt.Sprintf("Discovered node '%v'", node.Name), + done: true, + } + teaProgram.Send(progressNotification{ + progressLogs: toProgressLogs(discoveryProgressMap), + }) + } } if len(devices) >= len(nodes) { return @@ -284,13 +305,32 @@ func discoverMain(ctx context.Context) { dryRunPrinter(nodeList) return } - - resultMap, err := discoverDevices(ctx, nodes) + var teaProgram *tea.Program + var wg sync.WaitGroup + if !quietFlag { + m := newProgressModel(false) + teaProgram = tea.NewProgram(m) + wg.Add(1) + go func() { + defer wg.Done() + if _, err := teaProgram.Run(); err != nil { + fmt.Println("error running program:", err) + os.Exit(1) + } + }() + } + resultMap, err := discoverDevices(ctx, nodes, teaProgram) if err != nil { utils.Eprintf(quietFlag, true, "%v\n", err) os.Exit(1) } - + if teaProgram != nil { + teaProgram.Send(progressNotification{ + done: true, + err: err, + }) + wg.Wait() + } if err := showDevices(resultMap); err != nil { if !errors.Is(err, errDiscoveryFailed) { utils.Eprintf(quietFlag, true, "%v\n", err) diff --git a/cmd/kubectl-directpv/init.go b/cmd/kubectl-directpv/init.go index d64368584..292af73cb 100644 --- a/cmd/kubectl-directpv/init.go +++ b/cmd/kubectl-directpv/init.go @@ -21,8 +21,10 @@ import ( "fmt" "os" "strings" + "sync" "time" + tea "github.com/charmbracelet/bubbletea" "github.com/fatih/color" "github.com/google/uuid" "github.com/jedib0t/go-pretty/v6/table" @@ -110,6 +112,9 @@ func toInitRequestObjects(config *InitConfig, requestID string) (initRequests [] } func showResults(results []initResult) { + if len(results) == 0 { + return + } writer := newTableWriter( table.Row{ "REQUEST_ID", @@ -164,14 +169,33 @@ func showResults(results []initResult) { writer.Render() } -func initDevices(ctx context.Context, initRequests []types.InitRequest, requestID string) (results []initResult, err error) { - var totalReqCount int +func toProgressLogs(progressMap map[string]progressLog) (logs []progressLog) { + for _, v := range progressMap { + logs = append(logs, v) + } + return +} + +func initDevices(ctx context.Context, initRequests []types.InitRequest, requestID string, teaProgram *tea.Program) (results []initResult, err error) { + totalReqCount := len(initRequests) + totalTasks := totalReqCount * 2 + var completedTasks int + initProgressMap := make(map[string]progressLog, totalReqCount) for i := range initRequests { - _, err := client.InitRequestClient().Create(ctx, &initRequests[i], metav1.CreateOptions{TypeMeta: types.NewInitRequestTypeMeta()}) + initReq, err := client.InitRequestClient().Create(ctx, &initRequests[i], metav1.CreateOptions{TypeMeta: types.NewInitRequestTypeMeta()}) if err != nil { return nil, err } - totalReqCount++ + if teaProgram != nil { + completedTasks++ + initProgressMap[initReq.Name] = progressLog{ + log: fmt.Sprintf("Processing initialization request '%s' for node '%v'", initReq.Name, initReq.GetNodeID()), + } + teaProgram.Send(progressNotification{ + progressLogs: toProgressLogs(initProgressMap), + percent: float64(completedTasks) / float64(totalTasks), + }) + } } ctx, cancel := context.WithTimeout(ctx, initRequestListTimeout) defer cancel() @@ -205,6 +229,17 @@ func initDevices(ctx context.Context, initRequests []types.InitRequest, requestI devices: initReq.Status.Results, failed: initReq.Status.Status == directpvtypes.InitStatusError, }) + if teaProgram != nil { + completedTasks++ + initProgressMap[initReq.Name] = progressLog{ + log: fmt.Sprintf("Processed initialization request '%s' for node '%v'", initReq.Name, initReq.GetNodeID()), + done: true, + } + teaProgram.Send(progressNotification{ + progressLogs: toProgressLogs(initProgressMap), + percent: float64(completedTasks) / float64(totalTasks), + }) + } } if len(results) >= totalReqCount { return @@ -249,10 +284,31 @@ func initMain(ctx context.Context, inputFile string) { LabelSelector: directpvtypes.ToLabelSelector(labelMap), }) }() - results, err := initDevices(ctx, initRequests, requestID) - if err != nil { + var teaProgram *tea.Program + var wg sync.WaitGroup + if !quietFlag { + m := newProgressModel(true) + teaProgram = tea.NewProgram(m) + wg.Add(1) + go func() { + defer wg.Done() + if _, err := teaProgram.Run(); err != nil { + fmt.Println("error running program:", err) + os.Exit(1) + } + }() + } + results, err := initDevices(ctx, initRequests, requestID, teaProgram) + if err != nil && quietFlag { utils.Eprintf(quietFlag, true, "%v\n", err) os.Exit(1) } + if teaProgram != nil { + teaProgram.Send(progressNotification{ + done: true, + err: err, + }) + wg.Wait() + } showResults(results) } diff --git a/cmd/kubectl-directpv/install.go b/cmd/kubectl-directpv/install.go index bf6f4a024..27f590a7b 100644 --- a/cmd/kubectl-directpv/install.go +++ b/cmd/kubectl-directpv/install.go @@ -24,7 +24,6 @@ import ( "sync" "time" - "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/table" @@ -77,7 +76,10 @@ var installCmd = &cobra.Command{ $ kubectl {PLUGIN_NAME} install -o yaml > directpv-install.yaml 6. Install DirectPV with apparmor profile - $ kubectl {PLUGIN_NAME} install --apparmor-profile apparmor.json`, + $ kubectl {PLUGIN_NAME} install --apparmor-profile directpv + +7. Install DirectPV with seccomp profile + $ kubectl {PLUGIN_NAME} install --seccomp-profile profiles/seccomp.json`, `{PLUGIN_NAME}`, consts.AppName, ), @@ -289,9 +291,7 @@ func installMain(ctx context.Context) { var installedComponents []installer.Component var wg sync.WaitGroup if dryRunPrinter == nil && !quietFlag { - m := progressModel{ - model: progress.New(progress.WithGradient("#FFFFFF", "#FFFFFF")), - } + m := newProgressModel(true) teaProgram := tea.NewProgram(m) wg.Add(1) go func() { diff --git a/cmd/kubectl-directpv/progress_model.go b/cmd/kubectl-directpv/progress_model.go index 87dc4da40..fa762e1e6 100644 --- a/cmd/kubectl-directpv/progress_model.go +++ b/cmd/kubectl-directpv/progress_model.go @@ -22,29 +22,52 @@ import ( "time" "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/fatih/color" ) const ( padding = 1 maxWidth = 80 + tick = "✔" ) +type progressLog struct { + log string + done bool +} + type progressNotification struct { - log string - message string - percent float64 - done bool - err error + log string + progressLogs []progressLog + message string + percent float64 + done bool + err error } type progressModel struct { - model progress.Model - message string - logs []string - done bool - err error + model *progress.Model + spinner spinner.Model + message string + progressLogs []progressLog + logs []string + done bool + err error +} + +func newProgressModel(withProgressBar bool) *progressModel { + progressM := &progressModel{} + progressM.spinner = spinner.New() + progressM.spinner.Spinner = spinner.Points + progressM.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#F7971E")) + if withProgressBar { + progress := progress.New(progress.WithDefaultGradient()) + progressM.model = &progress + } + return progressM } func finalPause() tea.Cmd { @@ -54,15 +77,17 @@ func finalPause() tea.Cmd { } func (m progressModel) Init() tea.Cmd { - return nil + return m.spinner.Tick } func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - m.model.Width = msg.Width - padding*2 - 4 - if m.model.Width > maxWidth { - m.model.Width = maxWidth + if m.model != nil { + m.model.Width = msg.Width - padding*2 - 4 + if m.model.Width > maxWidth { + m.model.Width = maxWidth + } } return m, nil @@ -74,6 +99,9 @@ func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logs = append(m.logs, msg.log) } } + if len(msg.progressLogs) > 0 { + m.progressLogs = msg.progressLogs + } m.message = msg.message if msg.err != nil { m.err = msg.err @@ -84,30 +112,54 @@ func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.done = msg.done cmds = append(cmds, tea.Sequence(finalPause(), tea.Quit)) } - if msg.percent > 0.0 { + if m.model != nil && msg.percent > 0.0 { cmds = append(cmds, m.model.SetPercent(msg.percent)) } return m, tea.Batch(cmds...) // FrameMsg is sent when the progress bar wants to animate itself case progress.FrameMsg: - progressModel, cmd := m.model.Update(msg) - m.model = progressModel.(progress.Model) - return m, cmd + if m.model != nil { + progressModel, cmd := m.model.Update(msg) + pModel := progressModel.(progress.Model) + m.model = &pModel + return m, cmd + } + return m, nil default: - return m, nil + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd } } func (m progressModel) View() (str string) { pad := strings.Repeat(" ", padding) - str = "\n" + pad + m.model.View() + "\n\n" + str = "\n" + if m.model != nil { + str = str + pad + m.model.View() + "\n\n" + } if !m.done { - str += pad + fmt.Sprintf("%s \n\n", m.message) + if m.message != "" { + str += pad + fmt.Sprintf("%s \n\n", m.message) + } + } + for i := range m.progressLogs { + if m.progressLogs[i].done { + str += pad + fmt.Sprintf("%s %s\n", color.HiYellowString(m.progressLogs[i].log), m.spinner.Style.Render(tick)) + } else { + str += pad + fmt.Sprintf("%s %s\n", color.HiYellowString(m.progressLogs[i].log), m.spinner.View()) + } + if i == len(m.progressLogs)-1 { + str += "\n" + } } for i := range m.logs { - str += pad + color.HiYellowString(fmt.Sprintf("%s \n\n", m.logs[i])) + str += pad + color.HiYellowString(fmt.Sprintf("%s \n", m.logs[i])) + if i == len(m.logs)-1 { + str += "\n" + } } if m.err != nil { str += pad + color.HiRedString("Error; %s \n\n", m.err.Error()) diff --git a/functests/common.sh b/functests/common.sh index 56267f8fc..2414a6b0f 100644 --- a/functests/common.sh +++ b/functests/common.sh @@ -130,13 +130,13 @@ function add_drives() { config_file="$(mktemp)" - if ! "${DIRECTPV_CLIENT}" discover --output-file "${config_file}" > /tmp/.output 2>&1; then + if ! "${DIRECTPV_CLIENT}" discover --quiet --output-file "${config_file}" > /tmp/.output 2>&1; then cat /tmp/.output echo "$ME: error: failed to discover the devices" rm "${config_file}" return 1 fi - if ! echo Yes | "${DIRECTPV_CLIENT}" init "${config_file}" > /tmp/.output 2>&1; then + if ! echo Yes | "${DIRECTPV_CLIENT}" init --quiet "${config_file}" > /tmp/.output 2>&1; then cat /tmp/.output echo "$ME: error: failed to initialize the drives" rm "${config_file}"