Skip to content

Commit

Permalink
Another code refactor
Browse files Browse the repository at this point in the history
This project has seen its use of several framework for both UI and
the Kubernetes client controller. This time, going back to simple
informer model with no controller framework.  This approach seems
simpler and works well with the UI without the complication of full
controller kit (like controller-runtime).

This refactor also updates the way the program works by implementing
the ability to display either usage metrics (from Metrics Server) or
request/limit resource metrics from the API server. That way, the UI
is always functional regardless of how the cluster is setup.
  • Loading branch information
vladimirvivien committed Dec 28, 2021
1 parent 0b188c8 commit 0ba76d9
Show file tree
Hide file tree
Showing 28 changed files with 1,588 additions and 1,252 deletions.
20 changes: 10 additions & 10 deletions application/app_ctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,22 @@ func (app *Application) GetStopChan() <-chan struct{} {
return app.stopCh
}

// ShowBanner displays a welcome banner
func (app *Application) WelcomeBanner() {
fmt.Println(`
_ _
| | _| |_ ___ _ __
| |/ / __/ _ \| '_ \
_ _
| | _| |_ ___ _ __
| |/ / __/ _ \| '_ \
| <| || (_) | |_) |
|_|\_\\__\___/| .__/
|_|\_\\__\___/| .__/
|_|`)
fmt.Println("Version 0.1.0-alpha.1")
}

func (app *Application) setup() error {
func (app *Application) setup(ctx context.Context) error {
// setup each page panel
for _, page := range app.pages {
if err := page.Panel.Run(); err != nil {
// TODO propagate context to panel run
if err := page.Panel.Run(ctx); err != nil {
return fmt.Errorf("init failed: page %s: %s", page.Title, err)
}
}
Expand All @@ -99,15 +99,15 @@ func (app *Application) setup() error {

var hdr strings.Builder
hdr.WriteString("%c [green]API server: [white]%s [green]namespace: [white]%s [green] metrics:")
if err := app.k8sClient.AssertMetricsAvailable(); err != nil {
if err := app.GetK8sClient().AssertMetricsAvailable(); err != nil {
hdr.WriteString(" [red]not connected")
} else {
hdr.WriteString(" [white]connected")
}

app.panel.DrawHeader(fmt.Sprintf(
hdr.String(),
ui.Icons.Rocket, app.k8sClient.Config().Host, app.k8sClient.Namespace(),
ui.Icons.Rocket, app.GetK8sClient().Config().Host, app.k8sClient.Namespace(),
))

app.panel.DrawFooter(app.getPageTitles()[app.visibleView])
Expand Down Expand Up @@ -147,7 +147,7 @@ func (app *Application) setup() error {
func (app *Application) Run(ctx context.Context) error {

// setup application UI
if err := app.setup(); err != nil {
if err := app.setup(ctx); err != nil {
return err
}

Expand Down
50 changes: 42 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
module github.com/vladimirvivien/ktop

require (
github.com/gdamore/tcell v1.1.2
github.com/gdamore/tcell/v2 v2.1.0
github.com/rivo/tview v0.0.0-20201204190810-5406288b8e4e
k8s.io/api v0.20.0
k8s.io/apimachinery v0.20.1
k8s.io/client-go v0.20.0
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
github.com/rivo/tview v0.0.0-20211202162923-2a6de950f73b
k8s.io/api v0.23.1
k8s.io/apimachinery v0.23.1
k8s.io/client-go v0.23.1
k8s.io/metrics v0.19.0
sigs.k8s.io/controller-runtime v0.7.0
)

go 1.15
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/imdario/mergo v0.3.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

go 1.17
652 changes: 257 additions & 395 deletions go.sum

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion hack/kind-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
Expand Down
11 changes: 7 additions & 4 deletions hack/metrics-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ subjects:
name: metrics-server
namespace: kube-system
---
apiVersion: apiregistration.k8s.io/v1beta1
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.metrics.k8s.io
Expand Down Expand Up @@ -96,15 +96,18 @@ spec:
emptyDir: {}
containers:
- name: metrics-server
image: k8s.gcr.io/metrics-server/metrics-server:v0.3.7
image: gcr.io/k8s-staging-metrics-server/metrics-server:master
imagePullPolicy: IfNotPresent
args:
- --cert-dir=/tmp
- --secure-port=4443
- --secure-port=443
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=15s
- --kubelet-insecure-tls
ports:
- name: main-port
containerPort: 4443
containerPort: 443
protocol: TCP
securityContext:
readOnlyRootFilesystem: true
Expand Down
6 changes: 3 additions & 3 deletions hack/minikube.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/sh

minikube start --memory=2192 --cpus=2 \
--kubernetes-version=v1.20.0 \
--vm-driver=docker \
minikube start --memory=4096 --cpus=2 \
--kubernetes-version=v1.23.1 \
--vm-driver=hyperkit \
--bootstrapper=kubeadm \
--extra-config=apiserver.enable-admission-plugins="LimitRanger,NamespaceExists,NamespaceLifecycle,ResourceQuota,ServiceAccount,DefaultStorageClass,MutatingAdmissionWebhook"
14 changes: 14 additions & 0 deletions hack/persistent-volume-local.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-volume
labels:
type: local
spec:
storageClassName: local-storage
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data"
34 changes: 7 additions & 27 deletions k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package k8s
import (
"context"
"fmt"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
Expand All @@ -15,22 +15,15 @@ import (
metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
)

var (
NodesResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "nodes"}
PodsResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
)

type ListFunc func(ctx context.Context, namespace string, list runtime.Object) error

type Client struct {
namespace string
config *restclient.Config
dynaClient dynamic.Interface
discoClient *discovery.DiscoveryClient
metricsClient *metricsclient.Clientset
metricsAvailable bool
nodeWatcher *NodeWatcher
podWatcher *PodWatcher
refreshTimeout time.Duration
controller *Controller
}

func New(ctx context.Context, kubeconfig, kubectx, namespace string) (*Client, error) {
Expand Down Expand Up @@ -61,16 +54,7 @@ func New(ctx context.Context, kubeconfig, kubectx, namespace string) (*Client, e
discoClient: disco,
metricsClient: metrics,
}

client.nodeWatcher = NewNodeWatcher(client)
if err := client.nodeWatcher.Start(ctx); err != nil {
return nil, err
}
client.podWatcher = NewPodWatcher(client)
if err := client.podWatcher.Start(ctx); err != nil {
return nil, err
}

client.controller = newController(client)
return client, nil
}

Expand Down Expand Up @@ -138,10 +122,6 @@ func (k8s *Client) GetPodMetrics(ctx context.Context, podName string) (*metricsV
return metrics, nil
}

func (k8s *Client) AddNodeListFunc(f ListFunc) {
k8s.nodeWatcher.AddNodeListFunc(f)
}

func (k8s *Client) AddPodListFunc(f ListFunc) {
k8s.podWatcher.AddPodListFunc(f)
}
func (k8s *Client) Controller() *Controller {
return k8s.controller
}
103 changes: 103 additions & 0 deletions k8s/client_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package k8s

import (
"context"
"errors"
"fmt"
"time"

"github.com/vladimirvivien/ktop/views/model"
appsV1 "k8s.io/api/apps/v1"
batchV1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
)

var (
GVRs = map[string]schema.GroupVersionResource{
"nodes": {Group: "", Version: "v1", Resource: "nodes"},
"namespaces": {Group: "", Version: "v1", Resource: "namespaces"},
"pods": {Group: "", Version: "v1", Resource: "pods"},
"persistentvolumeclaims": {Group: "", Version: "v1", Resource: "persistentvolumeclaims"},
"deployments": {Group: appsV1.GroupName, Version: "v1", Resource: "deployments"},
"daemonsets": {Group: appsV1.GroupName, Version: "v1", Resource: "daemonsets"},
"replicasets": {Group: appsV1.GroupName, Version: "v1", Resource: "replicasets"},
"statefulsets": {Group: appsV1.GroupName, Version: "v1", Resource: "statefulsets"},
"jobs": {Group: batchV1.GroupName, Version: "v1", Resource: "jobs"},
"cronjobs": {Group: batchV1.GroupName, Version: "v1", Resource: "cronjobs"},
}
)

type RefreshNodesFunc func(ctx context.Context, items []model.NodeModel) error
type RefreshPodsFunc func(ctx context.Context, items []model.PodModel) error
type RefreshSummaryFunc func(ctx context.Context, items model.ClusterSummary) error

type Controller struct {
client *Client
namespaceInformer informers.GenericInformer
nodeInformer informers.GenericInformer
nodeRefreshFunc RefreshNodesFunc
podInformer informers.GenericInformer
podRefreshFunc RefreshPodsFunc

deploymentInformer informers.GenericInformer
daemonSetInformer informers.GenericInformer
replicaSetInformer informers.GenericInformer
statefulSetInformer informers.GenericInformer

jobInformer informers.GenericInformer
cronJobInformer informers.GenericInformer

pvcInformer informers.GenericInformer
summaryRefreshFunc RefreshSummaryFunc
}

func newController(client *Client) *Controller {
ctrl := &Controller{client: client}
return ctrl
}

func (c *Controller) SetNodeRefreshFunc(fn RefreshNodesFunc) *Controller {
c.nodeRefreshFunc = fn
return c
}
func (c *Controller) SetPodRefreshFunc(fn RefreshPodsFunc) *Controller {
c.podRefreshFunc = fn
return c
}

func (c *Controller) SetClusterSummaryRefreshFunc(fn RefreshSummaryFunc) *Controller {
c.summaryRefreshFunc = fn
return c
}

func (c *Controller) Start(ctx context.Context, resync time.Duration) error {
if ctx == nil {
return errors.New("context cannot be nil")
}
factory := dynamicinformer.NewDynamicSharedInformerFactory(c.client.dynaClient, resync)
c.namespaceInformer = factory.ForResource(GVRs["namespaces"])
c.nodeInformer = factory.ForResource(GVRs["nodes"])
c.podInformer = factory.ForResource(GVRs["pods"])
c.deploymentInformer = factory.ForResource(GVRs["deployments"])
c.daemonSetInformer = factory.ForResource(GVRs["daemonsets"])
c.replicaSetInformer = factory.ForResource(GVRs["replicasets"])
c.statefulSetInformer = factory.ForResource(GVRs["statefulsets"])
c.jobInformer = factory.ForResource(GVRs["jobs"])
c.cronJobInformer = factory.ForResource(GVRs["cronjobs"])
c.pvcInformer = factory.ForResource(GVRs["persistentvolumeclaims"])
//c.installHandler(ctx, c.pvcInformer, c.pvcRefreshFunc)

factory.Start(ctx.Done())
for name, gvr := range GVRs {
if synced := factory.WaitForCacheSync(ctx.Done()); !synced[gvr] {
return fmt.Errorf("resource not synced: %s", name)
}
}
c.setupSummaryHandler(ctx, c.summaryRefreshFunc)
c.setupNodeHandler(ctx, c.nodeRefreshFunc)
c.installPodsHandler(ctx, c.podRefreshFunc)

return nil
}
Loading

0 comments on commit 0ba76d9

Please sign in to comment.