diff --git a/README.md b/README.md index a71454c..7cae6de 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,51 @@

Kaytu Logo - -

Kaytu enables engineering, DevOps, and SRE teams to reduce cloud costs by recommending optimal workload configurations based on actual-usage, ensuring savings without compromise. +

Kaytu recommends optimal workload configurations based on actual-usage,, ensuring savings without compromising reliability.

![Kaytu Gif](.github/assets/kaytu.gif) - ## Overview - - - **Ease of use**: One-line command. Use without modifying workloads or making configuration changes. -- **Base on actual Usage**: Analyzes the past seven days of usage from Cloud native monitoring (CloudWatch), including advanced AWS CloudWatch metrics (where available). +- **Optimize**: Optimize AWS EC2 Instances & AWS RDS Instances/Clusters. +- **Base on actual Usage**: Analyzes the past seven days of usage from Cloud native monitoring (CloudWatch). - **Customize**: Optimize for region, CPU, memory, network performance, storage, licenses, and more to match your specific requirements. - **Secure** - no credentials to share; extracts required metrics from the client side -- **Open-core philosophy** Use without fear of lock-in. The CLI is open-sourced, and the Server side will be open-sourced soon. -- **Coming Soon**: Non-Interactive mode, Azure support, GPU Optimization, Credit utilization for Burst instances, and Observability data from Prometheus - +- **Open philosophy** Use without fear of lock-in. The CLI is open-sourced, and the Server side will be open-sourced soon. +- **Coming Soon**: Non-Interactive mode, GCP, Azure, GPU Optimization and Observability data from Prometheus ## Getting Started - ### 1. Install Kaytu CLI - **MacOS** ```shell brew tap kaytu-io/cli-tap && brew install kaytu ``` - **Linux** ```shell curl -fsSL https://raw.githubusercontent.com/kaytu-io/kaytu/main/scripts/install.sh | sh ``` - **Windows (and all Binaries)** Download Windows (Linux, and MacOS) binary from [releases](https://github.com/kaytu-io/kaytu/releases) - - ### 2. Login to AWS CLI - Kaytu works with your existing AWS CLI profile (read-only access required) to gather metrics. - To confirm your AWS CLI login is working correctly: - ``` aws sts get-caller-identity ``` [Click here to see how to log in to AWS CLI.](https://docs.aws.amazon.com/signin/latest/userguide/command-line-sign-in.html) - We respect your privacy. Our open-source code guarantees that we never collect sensitive information like AWS credentials, IPs, tags, etc. - ### 3. Run Kaytu CLI - ```shell kaytu ``` - -it will install aws plugin automatically. -now you can get optimizations by running: - +This will run and install any plugins. +To see how you can optimize EC2 Instances, run this command: ```shell kaytu optimize ec2-instance ``` - +Some optimizations such as RDS require login: ```shell kaytu login - +``` +For RDS: +```shell kaytu optimize rds-instance ``` diff --git a/cmd/root.go b/cmd/root.go index 152c59b..8d27390 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -192,13 +192,13 @@ func Execute() { err := manager.NonInteractiveView.WaitAndShowResults(nonInteractiveFlag, csvExportFlag, jsonExportFlag) return err } else { - jobsController := controller.NewJobs() - statusBar := view.NewStatusBarView(jobsController) - jobsPage := view.NewJobsPage(jobsController) - helpController := controller.NewHelp() helpPage := view.NewHelpPage(helpController) + jobsController := controller.NewJobs() + statusBar := view.NewStatusBarView(jobsController, helpController) + jobsPage := view.NewJobsPage(jobsController) + optimizationsController := controller.NewOptimizations() optimizationsPage := view.NewOptimizationsView(optimizationsController, helpController, statusBar) optimizationsDetailsPage := view.NewOptimizationDetailsView(optimizationsController, helpController, statusBar) diff --git a/pkg/style/style.go b/pkg/style/style.go index 049c8c0..60c84c0 100644 --- a/pkg/style/style.go +++ b/pkg/style/style.go @@ -29,5 +29,7 @@ var ( StatusBarStyle = lipgloss.NewStyle().Background(lipgloss.Color("#222222")).Foreground(lipgloss.Color("#ffffff")).Width(9999) JobsStatusStyle = lipgloss.NewStyle().Background(lipgloss.Color("#dd5200")).Foreground(lipgloss.Color("#ffffff")) ErrorStatusStyle = lipgloss.NewStyle().Background(lipgloss.Color("#aa2222")).Foreground(lipgloss.Color("#ffffff")) + InfoStatusStyle = lipgloss.NewStyle().Background(lipgloss.Color("#3a3835")).Foreground(lipgloss.Color("#ffffff")) + InfoStatusStyle2 = lipgloss.NewStyle().Background(lipgloss.Color("#006d69")).Foreground(lipgloss.Color("#ffffff")) ) diff --git a/view/app.go b/view/app.go index 562b18e..6255179 100644 --- a/view/app.go +++ b/view/app.go @@ -77,6 +77,7 @@ func (m *App) ChangePage(id PageEnum) tea.Cmd { } func (m *App) Init() tea.Cmd { + m.ChangePage(Page_Optimizations) return tea.Batch(m.pages[m.activePageIdx].Init(), tea.EnterAltScreen, TickCmdWithDuration(100*time.Microsecond)) } @@ -103,8 +104,8 @@ func (m *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": return m, tea.Quit - case "ctrl+h": - changePageCmd = tea.Batch(changePageCmd, m.ChangePage(Page_Help)) + //case "ctrl+h": + // changePageCmd = tea.Batch(changePageCmd, m.ChangePage(Page_Help)) case "ctrl+j": changePageCmd = tea.Batch(changePageCmd, m.ChangePage(Page_Jobs)) case "esc": diff --git a/view/page_optimization.go b/view/page_optimization.go index 3cf3d90..9957850 100644 --- a/view/page_optimization.go +++ b/view/page_optimization.go @@ -53,19 +53,21 @@ func (m OptimizationsPage) OnClose() Page { return m } func (m OptimizationsPage) OnOpen() Page { - return m -} - -func (m OptimizationsPage) Init() tea.Cmd { m.helpController.SetKeyMap([]string{ "↑/↓: move", - "←/→: scroll right and left in the table", - "enter: see details", - "p: change preferences for one item", - "P: change preferences for all items", + "pgdown/pgup: next/prev page", + "←/→: scroll in the table", + "enter: see resource details", + "p: change preferences", + "P: change preferences for all resources", "r: load all items in current page", + "ctrl+j: list of jobs", "q/ctrl+c: exit", }) + return m +} + +func (m OptimizationsPage) Init() tea.Cmd { return nil } @@ -183,7 +185,7 @@ func (m OptimizationsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newStatusBar, _ := m.statusBar.Update(msg) m.statusBar = newStatusBar.(StatusBarView) - m.table = m.table.WithPageSize(m.GetHeight() - 8).WithMaxTotalWidth(m.GetWidth()) + m.table = m.table.WithPageSize(m.GetHeight() - (7 + m.statusBar.Height())).WithMaxTotalWidth(m.GetWidth()) return m, cmd } diff --git a/view/page_optimization_details.go b/view/page_optimization_details.go index 2cd2dee..7bfb60f 100644 --- a/view/page_optimization_details.go +++ b/view/page_optimization_details.go @@ -165,7 +165,9 @@ func (m OptimizationDetailsPage) OnOpen() Page { m.selectedDevice = "" m.helpController.SetKeyMap([]string{ "↑/↓: move", - "esc/←: back to optimizations list", + "←/→: scroll in the table", + "enter: switch to device detail table", + "esc: back to optimizations list", "q/ctrl+c: exit", }) return m diff --git a/view/page_preferences_configuration.go b/view/page_preferences_configuration.go index 1b294c7..76410bf 100644 --- a/view/page_preferences_configuration.go +++ b/view/page_preferences_configuration.go @@ -176,7 +176,7 @@ func (m PreferencesConfigurationPage) View() string { //} //builder.WriteString(style.SvcDisable.Render(" ")) - visibleCount := m.GetHeight() - 5 + visibleCount := m.GetHeight() - (4 + m.statusBar.Height()) builder.WriteString("\n") if m.visibleStartIdx > 0 { builder.WriteString(" ⇡⇡⇡") @@ -247,7 +247,7 @@ func (m *PreferencesConfigurationPage) fixVisibleStartIdx() { m.visibleStartIdx-- } - visibleCount := m.GetHeight() - 5 + visibleCount := m.GetHeight() - (4 + m.statusBar.Height()) for m.focused >= m.visibleStartIdx+visibleCount { m.visibleStartIdx++ } diff --git a/view/view_statusbar.go b/view/view_statusbar.go index 7b67270..a376e36 100644 --- a/view/view_statusbar.go +++ b/view/view_statusbar.go @@ -9,39 +9,64 @@ import ( ) type StatusBarView struct { + helpController *controller.Help jobsController *controller.Jobs content string + width int } -func NewStatusBarView(JobsController *controller.Jobs) StatusBarView { - return StatusBarView{jobsController: JobsController} +func NewStatusBarView(JobsController *controller.Jobs, helpController *controller.Help) StatusBarView { + return StatusBarView{jobsController: JobsController, helpController: helpController} } func (v StatusBarView) Init() tea.Cmd { return nil } func (v StatusBarView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - runningCount, failedCount := len(v.jobsController.RunningJobs()), len(v.jobsController.FailedJobs()) + switch msg := msg.(type) { + case tea.WindowSizeMsg: + v.width = msg.Width + } + + failedJobs := v.jobsController.FailedJobs() + runningCount, failedCount := len(v.jobsController.RunningJobs()), len(failedJobs) var status []string + + var helpLines []string + w := 0 + + if runningCount > 0 { + line := fmt.Sprintf(" running jobs: %d ", runningCount) + w += len(line) + helpLines = append(helpLines, style.JobsStatusStyle.Render(line)) + } + + for idx, line := range v.helpController.Help() { + line = fmt.Sprintf(" %s ", line) + w += len(line) + if w > v.width { + helpLines = append(helpLines, "\n") + w = 0 + } + + if idx%2 == 0 { + helpLines = append(helpLines, style.InfoStatusStyle.Render(line)) + } else { + helpLines = append(helpLines, style.InfoStatusStyle2.Render(line)) + } + } + status = append(status, strings.Join(helpLines, "")+"\n") + if err := v.jobsController.GetError(); len(err) > 0 { status = append(status, style.ErrorStatusStyle.Render(strings.TrimSpace(err))+"\n") } - if runningCount > 0 { - status = append(status, style.JobsStatusStyle.Render(fmt.Sprintf(" running jobs: %d ", runningCount))) - } if failedCount > 0 { - status = append(status, style.JobsStatusStyle.Render(fmt.Sprintf(" failed jobs: %d ", failedCount))) + status = append(status, style.ErrorStatusStyle.Render(fmt.Sprintf("failed job: %s, press ctrl+j to see more", failedJobs[0]))+"\n") } - if runningCount > 0 || failedCount > 0 { - status = append(status, style.InfoStatusStyle.Render(fmt.Sprintf(" press ctrl+j to see list of jobs "))) - } - - status = append(status, style.InfoStatusStyle.Render(fmt.Sprintf(" press ctrl+h to see help page "))) - v.content = strings.Join(status, "") return v, nil } func (v StatusBarView) View() string { - return style.StatusBarStyle.Render(v.content) + return v.content } func (v StatusBarView) Height() int {