diff --git a/pkg/charm/models/panels/logs.go b/pkg/charm/models/panels/logs.go index f22d422..865a19f 100644 --- a/pkg/charm/models/panels/logs.go +++ b/pkg/charm/models/panels/logs.go @@ -47,9 +47,30 @@ func (m *Logs) AddContent(content string) { m.mutex.Lock() defer m.mutex.Unlock() m.content = strings.Join([]string{m.content, content}, "\n") - m.viewport.SetContent(m.content) + m.viewport.SetContent(wrapLogs(m.content, m.viewport.Width)) } func (m *Logs) Name() string { return m.name } + +func wrapLogs(logs string, maxWidth int) string { + splitLogs := strings.Split(logs, "\n") + var logsBuilder strings.Builder + for _, log := range splitLogs { + if len(log) > maxWidth { + segs := (len(log) / maxWidth) + for seg := 0; seg < segs; seg++ { + logsBuilder.WriteString(log[:maxWidth]) + logsBuilder.WriteString("\n") + log = log[maxWidth:] + } + //write any leftovers + logsBuilder.WriteString(log) + } else { + logsBuilder.WriteString(log) + } + logsBuilder.WriteString("\n") + } + return logsBuilder.String() +} diff --git a/pkg/charm/models/tabber.go b/pkg/charm/models/tabber.go index 9a3a292..0a9d157 100644 --- a/pkg/charm/models/tabber.go +++ b/pkg/charm/models/tabber.go @@ -130,11 +130,12 @@ func (p *pager) setPages(tabs []Tab, selected int, width int) { renderedTab = styles.SelectedTabStyle().Render(tab.Name) } tempTab = lipgloss.JoinHorizontal(lipgloss.Top, tempTab, renderedTab) - if lipgloss.Width(lipgloss.JoinHorizontal(lipgloss.Top, p.tabLeftArrow, tempTab, p.tabRightArrow)) > width-2 { + joined := lipgloss.JoinHorizontal(lipgloss.Bottom, p.tabLeftArrow, tempTab, p.tabRightArrow) + if lipgloss.Width(joined) > width-5 { tempPage.end = i tabPages = append(tabPages, tempPage) tempPage = page{start: i, tabs: []string{}} - tempTab = "" + tempTab = lipgloss.JoinHorizontal(lipgloss.Top, "", renderedTab) } tempPage.tabs = append(tempPage.tabs, renderedTab) diff --git a/pkg/paneler/item.go b/pkg/paneler/item.go index a63b1c1..cf3cdb6 100644 --- a/pkg/paneler/item.go +++ b/pkg/paneler/item.go @@ -20,8 +20,6 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/rest" - "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/cache" "sigs.k8s.io/yaml" ) @@ -34,28 +32,12 @@ type Item struct { restMapper meta.RESTMapper } -func NewItem(cfg *rest.Config) (*Item, error) { - client, err := dynamic.NewForConfig(cfg) - if err != nil { - return nil, fmt.Errorf("error creating dynamic client: %w", err) - } - - di, err := discovery.NewDiscoveryClientForConfig(cfg) - if err != nil { - return nil, fmt.Errorf("error creating discovery client: %w", err) - } - - gr, err := restmapper.GetAPIGroupResources(di) - if err != nil { - return nil, fmt.Errorf("error getting API group resources: %w", err) - } - - rm := restmapper.NewDiscoveryRESTMapper(gr) +func NewItem(dynamicClient dynamic.Interface, discoveryClient *discovery.DiscoveryClient, restMapper meta.RESTMapper) *Item { return &Item{ - dynamicClient: client, - discoveryClient: di, - restMapper: rm, - }, nil + dynamicClient: dynamicClient, + discoveryClient: discoveryClient, + restMapper: restMapper, + } } func (t *Item) Model(panel types.Panel) (tea.Model, error) { diff --git a/pkg/paneler/logs.go b/pkg/paneler/logs.go index bfbb9f7..a2534ac 100644 --- a/pkg/paneler/logs.go +++ b/pkg/paneler/logs.go @@ -5,20 +5,38 @@ import ( "context" "encoding/json" "fmt" - "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/everettraven/buoy/pkg/charm/models/panels" "github.com/everettraven/buoy/pkg/types" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" ) var _ Paneler = &Log{} type Log struct { - KubeClient *kubernetes.Clientset + typedClient *kubernetes.Clientset + dynamicClient dynamic.Interface + discoveryClient *discovery.DiscoveryClient + restMapper meta.RESTMapper +} + +func NewLog(typedClient *kubernetes.Clientset, dynamicClient dynamic.Interface, discoveryClient *discovery.DiscoveryClient, restMapper meta.RESTMapper) *Log { + return &Log{ + typedClient: typedClient, + dynamicClient: dynamicClient, + discoveryClient: discoveryClient, + restMapper: restMapper, + } } func (t *Log) Model(panel types.Panel) (tea.Model, error) { @@ -27,56 +45,90 @@ func (t *Log) Model(panel types.Panel) (tea.Model, error) { if err != nil { return nil, fmt.Errorf("unmarshalling panel to table type: %s", err) } - logItem := modelWrapperForLogPanel(t.KubeClient, log) - go streamLogs(t.KubeClient, log, logItem) //nolint: errcheck + logItem := modelWrapperForLogPanel(log) + pod, err := t.getPodForObject(log) + if err != nil { + return nil, fmt.Errorf("error getting pod for object: %w", err) + } + go streamLogs(t.typedClient, pod, logItem, log.Container) //nolint: errcheck return logItem, nil } -func modelWrapperForLogPanel(kc *kubernetes.Clientset, logsPanel types.Logs) *panels.Logs { +func modelWrapperForLogPanel(logsPanel types.Logs) *panels.Logs { vp := viewport.New(100, 20) vpw := panels.NewLogs(logsPanel.Name, vp) return vpw } -func streamLogs(kc *kubernetes.Clientset, logsPanel types.Logs, logItem *panels.Logs) { - //TODO: expand this beyond just a pod - req := kc.CoreV1().Pods(logsPanel.Key.Namespace).GetLogs(logsPanel.Key.Name, &v1.PodLogOptions{ - Container: logsPanel.Container, +func (t *Log) getPodForObject(logsPanel types.Logs) (*v1.Pod, error) { + gvk := schema.GroupVersionKind{ + Group: logsPanel.Group, + Version: logsPanel.Version, + Kind: logsPanel.Kind, + } + + if gvk == v1.SchemeGroupVersion.WithKind("Pod") { + return t.typedClient.CoreV1().Pods(logsPanel.Key.Namespace).Get(context.Background(), logsPanel.Key.Name, metav1.GetOptions{}) + } + + mapping, err := t.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return nil, fmt.Errorf("error creating resource mapping: %w", err) + } + u, err := t.dynamicClient.Resource(mapping.Resource).Namespace(logsPanel.Key.Namespace).Get(context.Background(), logsPanel.Key.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error getting object: %w", err) + } + + selector, err := getPodSelectorForUnstructured(u) + if err != nil { + return nil, fmt.Errorf("error getting pod selector for object: %w", err) + } + pods, err := t.typedClient.CoreV1().Pods(u.GetNamespace()).List(context.Background(), metav1.ListOptions{LabelSelector: selector.String()}) + if err != nil { + return nil, fmt.Errorf("error getting pods for object: %w", err) + } + if len(pods.Items) == 0 { + return nil, fmt.Errorf("no pods found for object") + } + return &pods.Items[0], nil +} + +func getPodSelectorForUnstructured(u *unstructured.Unstructured) (labels.Selector, error) { + selector, found, err := unstructured.NestedFieldCopy(u.Object, "spec", "selector") + if !found { + return nil, fmt.Errorf("no pod label selector found in object spec: %s", u.Object) + } + if err != nil { + return nil, fmt.Errorf("error getting pod label selector from object spec: %w", err) + } + sel := &metav1.LabelSelector{} + bytes, err := json.Marshal(selector) + if err != nil { + return nil, fmt.Errorf("error marshalling selector: %w", err) + } + err = json.Unmarshal(bytes, sel) + if err != nil { + return nil, fmt.Errorf("error unmarshalling selector: %w", err) + } + return metav1.LabelSelectorAsSelector(sel) +} + +func streamLogs(kc *kubernetes.Clientset, pod *v1.Pod, logItem *panels.Logs, container string) { + req := kc.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &v1.PodLogOptions{ + Container: container, Follow: true, }) rc, err := req.Stream(context.Background()) if err != nil { - logItem.AddContent(fmt.Errorf("fetching logs for %s/%s: %w", logsPanel.Key.Namespace, logsPanel.Key.Name, err).Error()) + logItem.AddContent(fmt.Errorf("fetching logs for %s/%s: %w", pod.Namespace, pod.Name, err).Error()) return } defer rc.Close() scanner := bufio.NewScanner(rc) for scanner.Scan() { - logs := wrapLogs(scanner.Bytes()) - logItem.AddContent(logs) + logItem.AddContent(scanner.Text()) } } - -func wrapLogs(logs []byte) string { - logStr := string(logs) - splitLogs := strings.Split(logStr, "\n") - var logsBuilder strings.Builder - for _, log := range splitLogs { - if len(log) > 100 { - segs := (len(log) / 100) - for seg := 0; seg < segs; seg++ { - logsBuilder.WriteString(log[:100]) - logsBuilder.WriteString("\n") - log = log[100:] - } - //write any leftovers - logsBuilder.WriteString(log) - } else { - logsBuilder.WriteString(log) - } - logsBuilder.WriteString("\n") - } - return logsBuilder.String() -} diff --git a/pkg/paneler/paneler.go b/pkg/paneler/paneler.go index ef42a2f..a58b825 100644 --- a/pkg/paneler/paneler.go +++ b/pkg/paneler/paneler.go @@ -5,8 +5,11 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/everettraven/buoy/pkg/types" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" ) type Paneler interface { @@ -25,26 +28,32 @@ func (p *paneler) Model(panel types.Panel) (tea.Model, error) { } func NewDefaultPaneler(cfg *rest.Config) (Paneler, error) { + dClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return nil, fmt.Errorf("error creating dynamic client: %w", err) + } + kubeClient, err := kubernetes.NewForConfig(cfg) if err != nil { return nil, fmt.Errorf("creating kubernetes.Clientset: %w", err) } - table, err := NewTable(cfg) + di, err := discovery.NewDiscoveryClientForConfig(cfg) if err != nil { - return nil, fmt.Errorf("creating table paneler: %w", err) + return nil, fmt.Errorf("error creating discovery client: %w", err) } - item, err := NewItem(cfg) + gr, err := restmapper.GetAPIGroupResources(di) if err != nil { - return nil, fmt.Errorf("creating item paneler: %w", err) + return nil, fmt.Errorf("error getting API group resources: %w", err) } + rm := restmapper.NewDiscoveryRESTMapper(gr) return &paneler{ panelerRegistry: map[string]Paneler{ - types.PanelTypeTable: table, - types.PanelTypeItem: item, - types.PanelTypeLogs: &Log{KubeClient: kubeClient}, + types.PanelTypeTable: NewTable(dClient, di, rm), + types.PanelTypeItem: NewItem(dClient, di, rm), + types.PanelTypeLogs: NewLog(kubeClient, dClient, di, rm), }, }, nil } diff --git a/pkg/paneler/table.go b/pkg/paneler/table.go index e354d73..2246107 100644 --- a/pkg/paneler/table.go +++ b/pkg/paneler/table.go @@ -18,8 +18,6 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/rest" - "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/cache" ) @@ -31,28 +29,12 @@ type Table struct { restMapper meta.RESTMapper } -func NewTable(cfg *rest.Config) (*Table, error) { - client, err := dynamic.NewForConfig(cfg) - if err != nil { - return nil, fmt.Errorf("error creating dynamic client: %w", err) - } - - di, err := discovery.NewDiscoveryClientForConfig(cfg) - if err != nil { - return nil, fmt.Errorf("error creating discovery client: %w", err) - } - - gr, err := restmapper.GetAPIGroupResources(di) - if err != nil { - return nil, fmt.Errorf("error getting API group resources: %w", err) - } - - rm := restmapper.NewDiscoveryRESTMapper(gr) +func NewTable(dynamicClient dynamic.Interface, discoveryClient *discovery.DiscoveryClient, restMapper meta.RESTMapper) *Table { return &Table{ - dynamicClient: client, - discoveryClient: di, - restMapper: rm, - }, nil + dynamicClient: dynamicClient, + discoveryClient: discoveryClient, + restMapper: restMapper, + } } func (t *Table) Model(panel buoytypes.Panel) (tea.Model, error) { diff --git a/test.json b/test.json index 0910727..775de6d 100644 --- a/test.json +++ b/test.json @@ -33,40 +33,6 @@ } ] }, - { - "name": "Kube-System Pods", - "group": "", - "version": "v1", - "kind": "Pod", - "type": "table", - "namespace": "kube-system", - "columns": [ - { - "header": "Namespace", - "path": "metadata.namespace" - }, - { - "header": "Name", - "path": "metadata.name" - }, - { - "header": "Phase", - "path": "status.phase" - }, - { - "header": "PodIP", - "path": "status.podIP" - }, - { - "header": "Start Time", - "path": "status.startTime" - }, - { - "header": "UID", - "path": "metadata.uid" - } - ] - }, { "name": "Kube-System Pods with label tier=control-plane", "group": "", @@ -104,20 +70,6 @@ } ] }, - { - "name": "ClusterRoles", - "group": "rbac.authorization.k8s.io", - "version": "v1", - "kind": "ClusterRole", - "type": "table", - "namespace": "kube-system", - "columns": [ - { - "header": "Name", - "path": "metadata.name" - } - ] - }, { "name": "Deployments", "group": "apps", @@ -175,25 +127,15 @@ ] }, { - "name": "Services", - "group": "", + "name": "CoreDNS Deployment Logs", + "group": "apps", "version": "v1", - "kind": "Service", - "type": "table", - "columns": [ - { - "header": "Namespace", - "path": "metadata.namespace" - }, - { - "header": "Name", - "path": "metadata.name" - }, - { - "header": "Type", - "path": "spec.type" - } - ] + "kind": "Deployment", + "type": "logs", + "key": { + "namespace": "kube-system", + "name": "coredns" + } } ] }