Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions cli/internal/kubehelpers/kubehelpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package kubehelpers

import (
"context"
"os"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

// Helper to get a Kubernetes client
func getKubeClient() (*kubernetes.Clientset, error) {
config, err := rest.InClusterConfig()
if err != nil {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = os.ExpandEnv("$HOME/.kube/config")
}
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
}
return kubernetes.NewForConfig(config)
}

// Generic function to get labels for a resource type
func GetResourceLabels(resourceType, namespace string) (labels []string, labelToName map[string][]string, err error) {
clientset, err := getKubeClient()
if err != nil {
return nil, nil, err
}
var items []metav1.Object
switch resourceType {
case "pod":
pods, err := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, nil, err
}
for i := range pods.Items {
items = append(items, &pods.Items[i])
}
case "deployment":
deploys, err := clientset.AppsV1().Deployments(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, nil, err
}
for i := range deploys.Items {
items = append(items, &deploys.Items[i])
}
case "daemonset":
daemonsets, err := clientset.AppsV1().DaemonSets(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, nil, err
}
for i := range daemonsets.Items {
items = append(items, &daemonsets.Items[i])
}
}
labelSet := make(map[string][]string)
for _, obj := range items {
for k, v := range obj.GetLabels() {
label := k + "=" + v
labelSet[label] = append(labelSet[label], obj.GetName())
}
}
labels = make([]string, 0, len(labelSet))
for label := range labelSet {
labels = append(labels, label)
}
return labels, labelSet, nil
}

// Get namespace labels
func GetNamespaceLabels() (labels []string, labelToNS map[string][]string, err error) {
clientset, err := getKubeClient()
if err != nil {
return nil, nil, err
}
nsList, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, nil, err
}
labelSet := make(map[string][]string)
for _, ns := range nsList.Items {

Check failure on line 87 in cli/internal/kubehelpers/kubehelpers.go

View workflow job for this annotation

GitHub Actions / Lint (linux, amd64)

rangeValCopy: each iteration copies 328 bytes (consider pointers or indexing) (gocritic)

Check failure on line 87 in cli/internal/kubehelpers/kubehelpers.go

View workflow job for this annotation

GitHub Actions / Lint (linux, arm64)

rangeValCopy: each iteration copies 328 bytes (consider pointers or indexing) (gocritic)

Check failure on line 87 in cli/internal/kubehelpers/kubehelpers.go

View workflow job for this annotation

GitHub Actions / Lint (windows, amd64)

rangeValCopy: each iteration copies 328 bytes (consider pointers or indexing) (gocritic)

Check failure on line 87 in cli/internal/kubehelpers/kubehelpers.go

View workflow job for this annotation

GitHub Actions / Lint (windows, arm64)

rangeValCopy: each iteration copies 328 bytes (consider pointers or indexing) (gocritic)
for k, v := range ns.Labels {
label := k + "=" + v
labelSet[label] = append(labelSet[label], ns.Name)
}
}
labels = make([]string, 0, len(labelSet))
for label := range labelSet {
labels = append(labels, label)
}
return labels, labelSet, nil
}
29 changes: 29 additions & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@ import (
)

func main() {
if len(os.Args) == 1 {
// No arguments: launch TUI
args, err := RunTUI()
if err != nil {
fmt.Println("TUI error:", err)
os.Exit(1)
}
if args != nil {
fmt.Printf("\nRetina CLI args: %s\n", args)
fmt.Print("\nWould you like to run this command? (y/n): ")
var confirm string
_, scanErr := fmt.Scanln(&confirm)
if scanErr != nil {
fmt.Println("Error reading input:", scanErr)
os.Exit(1)
}
if confirm == "y" || confirm == "Y" {
cmd.Retina.SetArgs(args)
if err := cmd.Retina.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
} else {
fmt.Println("Command not executed.")
}
}
// If args is nil, user cancelled or did not confirm; just exit
return
}
if err := cmd.Retina.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down
221 changes: 221 additions & 0 deletions cli/view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package main

import (
"fmt"
"sort"

"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
captureviews "github.com/microsoft/retina/cli/views/capture"
)

// Model step constants
const (
stepType selectStep = iota
stepNamespace
stepLabel
stepDone
)

type selectStep int

type model struct {
table table.Model
prompt string
selectedType string
selectedNamespace string
selectedLabel string
selectedNamespaceLabel string
nsSelectorForView string
step selectStep
labelOptions []string
labelToNS map[string][]string
labelToName map[string][]string
done bool // track if final state is reached
}

// Helper to create a table
func newTable(cols []table.Column, rows []table.Row, prompt string, height int) table.Model {

Check failure on line 38 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, amd64)

unused-parameter: parameter 'prompt' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 38 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, arm64)

unused-parameter: parameter 'prompt' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 38 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, amd64)

unused-parameter: parameter 'prompt' seems to be unused, consider removing or renaming it as _ (revive)

Check failure on line 38 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, arm64)

unused-parameter: parameter 'prompt' seems to be unused, consider removing or renaming it as _ (revive)
t := table.New(table.WithColumns(cols), table.WithRows(rows), table.WithFocused(true))
t.SetHeight(height)
return t
}

// Initial model
func initialModel() model {
// Get namespace label selectors for the initial prompt
labels, labelToNS, err := captureviews.GetNamespaceLabels()
if err != nil || len(labels) == 0 {
labels = []string{"none found"}
labelToNS = map[string][]string{"none found": {}}
}
rows := make([]table.Row, 0, len(labels))
for _, label := range labels {
if nsList, ok := labelToNS[label]; ok && len(nsList) > 0 {
rows = append(rows, table.Row{captureviews.JoinOrNone(nsList), label})
}
}
if len(rows) == 0 {
rows = append(rows, table.Row{"none found", "none found"})
}
// Sort rows by the left column (Namespace(s))
sort.Slice(rows, func(i, j int) bool {
return rows[i][0] < rows[j][0]
})
cols := []table.Column{{Title: "Namespace(s)", Width: 80}, {Title: "Namespace Label Selector", Width: 40}}
t := newTable(cols, rows, "Select a namespace label selector:", 15)
return model{
table: t,
prompt: "Select a namespace label selector:",
step: stepNamespace,
labelOptions: labels,
labelToNS: labelToNS,
}
}

// Add back toMM and fromMM helpers for model <-> MainModel conversion
func (m *model) toMM() captureviews.MainModel {
return captureviews.MainModel{
Table: m.table,
Prompt: m.prompt,
SelectedType: m.selectedType,
SelectedNamespace: m.selectedNamespace,
SelectedLabel: m.selectedLabel,
SelectedNamespaceLabel: m.selectedNamespaceLabel,
NsSelectorForView: m.nsSelectorForView,
Step: int(m.step),
LabelOptions: m.labelOptions,
LabelToNS: m.labelToNS,
LabelToName: m.labelToName,
}
}

func (m *model) fromMM(mm captureviews.MainModel) {
m.table = mm.Table
m.prompt = mm.Prompt
m.selectedType = mm.SelectedType
m.selectedNamespace = mm.SelectedNamespace
m.selectedLabel = mm.SelectedLabel
m.selectedNamespaceLabel = mm.SelectedNamespaceLabel
m.nsSelectorForView = mm.NsSelectorForView
m.step = selectStep(mm.Step)
m.labelOptions = mm.LabelOptions
m.labelToNS = mm.LabelToNS
m.labelToName = mm.LabelToName
}

// Main update logic, simplified
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {

Check failure on line 109 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, amd64)

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)

Check failure on line 109 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, arm64)

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)

Check failure on line 109 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, amd64)

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)

Check failure on line 109 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, arm64)

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)
case tea.KeyMsg:
switch msg.String() {
case "enter":
// Push current state to the stack before progressing
modelStack = append(modelStack, m)
mm := m.toMM()
switch m.step {

Check failure on line 116 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, amd64)

missing cases in switch of type main.selectStep: main.stepDone (exhaustive)

Check failure on line 116 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, arm64)

missing cases in switch of type main.selectStep: main.stepDone (exhaustive)

Check failure on line 116 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, amd64)

missing cases in switch of type main.selectStep: main.stepDone (exhaustive)

Check failure on line 116 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, arm64)

missing cases in switch of type main.selectStep: main.stepDone (exhaustive)
case stepType:
captureviews.HandleStepKubernetesResourceType(&mm)
case stepNamespace:
captureviews.HandleStepNamespace(&mm)
case stepLabel:
captureviews.HandleStepLabel(&mm)
}
m.fromMM(mm)
if m.step == stepDone {
m.done = true
return m, tea.Quit
}
return m, nil
case "q", "ctrl+c":
return m, tea.Quit
case "esc":
return goBackStack(), nil
}
}
m.table, _ = m.table.Update(msg)
return m, nil
}

// Add a global stack to persist model states for back navigation
var modelStack []model

// Go back one step in the TUI flow using the global stack
func goBackStack() model {
if len(modelStack) > 1 {
// Pop the current state
modelStack = modelStack[:len(modelStack)-1]
// Return the previous state
return modelStack[len(modelStack)-1]
} else if len(modelStack) == 1 {
// Only the initial state remains
return modelStack[0]
}
// If stack is empty, return a fresh initial model
return initialModel()
}

// Helper to build the CLI args for retina cobra
func buildArgs(ns, podSelector, nsSelector string) []string {
if ns == "" {
ns = "default"
}
return []string{
"capture", "create",
"--namespace", ns,
fmt.Sprintf("--pod-selectors=%s", podSelector),
fmt.Sprintf("--namespace-selectors=%s", nsSelector),
}
}

// RunTUI launches the TUI and returns the CLI args if the user confirms, or nil if cancelled.
func RunTUI() ([]string, error) {
m := initialModel()
modelStack = []model{m}
p := tea.NewProgram(m)
finalModel, err := p.Run()
if err != nil {
return nil, err
}
mFinal, ok := finalModel.(model)
if !ok {
return nil, fmt.Errorf("unexpected model type")

Check failure on line 182 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, amd64)

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"unexpected model type\")" (err113)

Check failure on line 182 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, arm64)

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"unexpected model type\")" (err113)

Check failure on line 182 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, amd64)

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"unexpected model type\")" (err113)

Check failure on line 182 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, arm64)

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"unexpected model type\")" (err113)
}
if mFinal.step == stepDone {
args := buildArgs(mFinal.selectedNamespace, mFinal.selectedLabel, mFinal.selectedNamespaceLabel)
return args, nil
}
return nil, nil // user cancelled or did not confirm
}

// Step handler for stepType
// moved to handler_step_type.go

// Step handler for stepNamespace
// moved to handler_step_namespace.go

// Step handler for stepLabel
// moved to handler_step_label.go

// Ensure model implements tea.Model
func (m model) Init() tea.Cmd { return nil }

// View logic remains unchanged
func (m model) View() string {
if m.done {
return ""
}
return fmt.Sprintf("%s\n\n%s\n\n(Use ↑/↓ to move, enter to select, q to quit)",
m.prompt,
m.table.View(),
)
}

func needsQuoting(s string) bool {

Check failure on line 214 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, amd64)

func `needsQuoting` is unused (unused)

Check failure on line 214 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (linux, arm64)

func `needsQuoting` is unused (unused)

Check failure on line 214 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, amd64)

func `needsQuoting` is unused (unused)

Check failure on line 214 in cli/view.go

View workflow job for this annotation

GitHub Actions / Lint (windows, arm64)

func `needsQuoting` is unused (unused)
for _, c := range s {
if c == ' ' || c == '\t' || c == '"' {
return true
}
}
return false
}
13 changes: 13 additions & 0 deletions cli/views/capture/handler_step_label.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package captureviews

// Exported handler for label step
func HandleStepLabel(m *MainModel) {
selected := m.Table.SelectedRow()
row := ToLabelRow(selected)
m.Prompt = "Confirm your selection:"
if row.LabelSelector != "" {
m.SelectedLabel = row.LabelSelector
m.Confirmed = true
m.Step = StepDone
}
}
Loading
Loading