Skip to content

Commit

Permalink
Batch of improvements (#24)
Browse files Browse the repository at this point in the history
* (feature+bugfix): fetch logs from resources with label selector in spec

and make log word wrap use the viewport width

Signed-off-by: Bryce Palmer <everettraven@gmail.com>

* (bugfix): fix bug where tab pages were rendered incorrectly

Signed-off-by: Bryce Palmer <everettraven@gmail.com>

* (cleanup): reuse clients instead of recreating them for each paneler type

Signed-off-by: Bryce Palmer <everettraven@gmail.com>

---------

Signed-off-by: Bryce Palmer <everettraven@gmail.com>
  • Loading branch information
everettraven authored Nov 15, 2023
1 parent 2b78acf commit 1e1ea17
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 156 deletions.
23 changes: 22 additions & 1 deletion pkg/charm/models/panels/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
5 changes: 3 additions & 2 deletions pkg/charm/models/tabber.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 5 additions & 23 deletions pkg/paneler/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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) {
Expand Down
120 changes: 86 additions & 34 deletions pkg/paneler/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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()
}
23 changes: 16 additions & 7 deletions pkg/paneler/paneler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
28 changes: 5 additions & 23 deletions pkg/paneler/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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) {
Expand Down
Loading

0 comments on commit 1e1ea17

Please sign in to comment.