Skip to content
Merged
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
22 changes: 10 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ LDFLAGS=-ldflags "-s -w \
-X 'github.com/zxh326/kite/pkg/version.CommitID=$(COMMIT_ID)'"

# Default target
.DEFAULT_GOAL := help
.DEFAULT_GOAL := build
DOCKER_TAG=latest

LOCALBIN ?= $(shell pwd)/bin
Expand Down Expand Up @@ -71,11 +71,15 @@ package-binaries: ## Package each kite binary file separately
done
@echo "✅ All kite binaries packaged successfully!"

frontend: ## Build frontend only
@echo "📦 Building frontend..."
cd $(UI_DIR) && npm run build
frontend: static ## Build frontend only

backend: ## Build backend only
static: ui/src/**/*.tsx ui/src/**/*.ts ui/index.html ui/**/*.css ui/package.json
@echo "📦 Ensuring static files are built..."
cd $(UI_DIR) && pnpm run build

backend: ${BINARY_NAME} ## Build backend only

$(BINARY_NAME): main.go pkg/**/*.go go.mod static
@echo "🏗️ Building backend..."
CGO_ENABLED=0 go build -trimpath $(LDFLAGS) -o $(BINARY_NAME) .

Expand All @@ -84,10 +88,8 @@ run: backend ## Run the built application
@echo "🚀 Starting $(BINARY_NAME) server..."
./$(BINARY_NAME)

dev: ## Run in development mode
dev: backend ## Run in development mode
@echo "🔄 Starting development mode..."
@echo "🏗️ Building backend..."
go build $(LDFLAGS) -o $(BINARY_NAME) .
@echo "🚀 Starting $(BINARY_NAME) server..."
./$(BINARY_NAME) -v=5 & \
BACKEND_PID=$$!; \
Expand Down Expand Up @@ -130,10 +132,6 @@ docs-build: ## Build documentation
@echo "📚 Building documentation..."
cd docs && pnpm run docs:build

release-helm-chart: ## Release Helm chart to GitHub Pages
@echo "🚀 Releasing Helm chart..."
./scripts/release-chart.sh $(shell git describe --tags --match 'v*' | grep -oE '[0-9]+\.[0-9][0-9]*(\.[0-9]+)?')

define go-install-tool
@[ -f "$(1)-$(3)" ] || { \
set -e; \
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ module github.com/zxh326/kite
go 1.24.3

require (
github.com/gin-contrib/gzip v1.2.3
github.com/gin-gonic/gin v1.10.1
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.66.1
github.com/samber/lo v1.51.0
golang.org/x/crypto v0.42.0
golang.org/x/net v0.44.0
gorm.io/driver/mysql v1.6.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
Expand Down Expand Up @@ -187,6 +189,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

_ "net/http/pprof"

"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/zxh326/kite/internal"
"github.com/zxh326/kite/pkg/auth"
Expand Down Expand Up @@ -188,6 +189,10 @@ func main() {
common.LoadEnvs()
gin.SetMode(gin.ReleaseMode)
r := gin.New()
if !common.DisableGZIP {
klog.Info("GZIP compression is enabled")
r.Use(gzip.Gzip(gzip.DefaultCompression))
}
r.Use(gin.Recovery())
r.Use(middleware.Logger())
r.Use(middleware.CORS())
Expand Down
5 changes: 5 additions & 0 deletions pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var (
AnonymousUserEnabled = false

CookieExpirationSeconds = 2 * JWTExpirationSeconds // double jwt

DisableGZIP = false
)

func LoadEnvs() {
Expand Down Expand Up @@ -75,4 +77,7 @@ func LoadEnvs() {
if v := os.Getenv("HOST"); v != "" {
Host = v
}
if v := os.Getenv("DISABLE_GZIP"); v == "true" {
DisableGZIP = true
}
}
28 changes: 28 additions & 0 deletions pkg/common/types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package common

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type SearchResult struct {
ID string `json:"id"`
Name string `json:"name"`
Expand Down Expand Up @@ -59,3 +64,26 @@ type ClusterInfo struct {
Version string `json:"version"`
IsDefault bool `json:"isDefault"`
}

type MetricsCell struct {
CPUUsage int64 `json:"cpuUsage,omitempty"`
CPULimit int64 `json:"cpuLimit,omitempty"`
CPURequest int64 `json:"cpuRequest,omitempty"`
MemoryUsage int64 `json:"memoryUsage,omitempty"`
MemoryLimit int64 `json:"memoryLimit,omitempty"`
MemoryRequest int64 `json:"memoryRequest,omitempty"`
}

type NodeWithMetrics struct {
*corev1.Node `json:",inline"`
Metrics *MetricsCell `json:"metrics"`
}

type NodeListWithMetrics struct {
Items []*NodeWithMetrics `json:"items"`
metav1.TypeMeta `json:",inline"`
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
}
19 changes: 1 addition & 18 deletions pkg/handlers/resources/generic_resource_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"k8s.io/kubectl/pkg/describe"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
)
Expand Down Expand Up @@ -471,21 +470,5 @@ func (h *GenericResourceHandler[T, V]) ListHistory(c *gin.Context) {
}

func (h *GenericResourceHandler[T, V]) Describe(c *gin.Context) {
cs := c.MustGet("cluster").(*cluster.ClientSet)
namespace := c.Param("namespace")
name := c.Param("name")
if h.Name() != "pods" {
return
}
pd := describe.PodDescriber{
Interface: cs.K8sClient.ClientSet,
}
out, err := pd.Describe(namespace, name, describe.DescriberSettings{
ShowEvents: true,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"result": out})
c.JSON(http.StatusOK, gin.H{"result": "not implemented"})
}
1 change: 1 addition & 0 deletions pkg/handlers/resources/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func registerClusterScopeRoutes(group *gin.RouterGroup, handler resourceHandler)
group.PUT("/_all/:name", handler.Update)
group.DELETE("/_all/:name", handler.Delete)
group.GET("/_all/:name/history", handler.ListHistory)
group.GET("/_all/:name/describe", handler.Describe)
}

func registerNamespaceScopeRoutes(group *gin.RouterGroup, handler resourceHandler) {
Expand Down
107 changes: 107 additions & 0 deletions pkg/handlers/resources/node_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import (
"context"
"fmt"
"net/http"
"sort"

"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/zxh326/kite/pkg/cluster"
"github.com/zxh326/kite/pkg/common"
"github.com/zxh326/kite/pkg/kube"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"k8s.io/kubectl/pkg/describe"
metricsv1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)

type NodeHandler struct {
Expand Down Expand Up @@ -243,6 +249,107 @@ func (h *NodeHandler) UntaintNode(c *gin.Context) {
})
}

func (h *NodeHandler) List(c *gin.Context) {
cs := c.MustGet("cluster").(*cluster.ClientSet)
var nodeMetrics metricsv1.NodeMetricsList

var nodes corev1.NodeList
if err := cs.K8sClient.List(c.Request.Context(), &nodes); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list nodes: " + err.Error()})
return
}

if err := cs.K8sClient.List(c.Request.Context(), &nodeMetrics); err != nil {
klog.Warningf("Failed to list node metrics: %v", err)
}

// Get all pods to calculate resource requests per node
var pods corev1.PodList
if err := cs.K8sClient.List(c.Request.Context(), &pods); err != nil {
klog.Warningf("Failed to list pods for node resource calculation: %v", err)
}

// Group pods by node name and calculate resource requests
nodeResourceRequests := make(map[string]common.MetricsCell)
for _, pod := range pods.Items {
if pod.Spec.NodeName == "" {
continue // Skip pods not scheduled to any node
}

nodeName := pod.Spec.NodeName
if _, exists := nodeResourceRequests[nodeName]; !exists {
nodeResourceRequests[nodeName] = common.MetricsCell{}
}

metrics := nodeResourceRequests[nodeName]

// Calculate CPU and memory requests for this pod
for _, container := range pod.Spec.Containers {
if cpuRequest := container.Resources.Requests.Cpu(); cpuRequest != nil {
metrics.CPURequest += cpuRequest.MilliValue()
}
if memoryRequest := container.Resources.Requests.Memory(); memoryRequest != nil {
metrics.MemoryRequest += memoryRequest.Value()
}
}

nodeResourceRequests[nodeName] = metrics
}

nodeMetricsMap := lo.KeyBy(nodeMetrics.Items, func(item metricsv1.NodeMetrics) string {
return item.Name
})

result := &common.NodeListWithMetrics{
TypeMeta: nodes.TypeMeta,
ListMeta: nodes.ListMeta,
Items: []*common.NodeWithMetrics{},
}
result.Items = make([]*common.NodeWithMetrics, len(nodes.Items))
for i, node := range nodes.Items {
metricsCell := &common.MetricsCell{}
metricsCell.CPULimit = node.Status.Allocatable.Cpu().MilliValue()
metricsCell.MemoryLimit = node.Status.Allocatable.Memory().Value()

if nm, ok := nodeMetricsMap[node.Name]; ok {
if cpuQuantity, ok := nm.Usage["cpu"]; ok {
metricsCell.CPUUsage = cpuQuantity.MilliValue()
}
if memQuantity, ok := nm.Usage["memory"]; ok {
metricsCell.MemoryUsage = memQuantity.Value()
}
}
if requests, exists := nodeResourceRequests[node.Name]; exists {
metricsCell.CPURequest = requests.CPURequest
metricsCell.MemoryRequest = requests.MemoryRequest
}
result.Items[i] = &common.NodeWithMetrics{
Node: &node,
Metrics: metricsCell,
}
}
sort.Slice(result.Items, func(i, j int) bool {
return result.Items[i].Name < result.Items[j].Name
})
c.JSON(http.StatusOK, result)
}

func (h *NodeHandler) Describe(c *gin.Context) {
cs := c.MustGet("cluster").(*cluster.ClientSet)
name := c.Param("name")
nd := describe.NodeDescriber{
Interface: cs.K8sClient.ClientSet,
}
out, err := nd.Describe("", name, describe.DescriberSettings{
ShowEvents: true,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"result": out})
}

func (h *NodeHandler) registerCustomRoutes(group *gin.RouterGroup) {
group.POST("/_all/:name/drain", h.DrainNode)
group.POST("/_all/:name/cordon", h.CordonNode)
Expand Down
Loading