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
62 changes: 32 additions & 30 deletions pkg/handlers/resources/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/zxh326/kite/pkg/common"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
Expand Down Expand Up @@ -44,35 +45,36 @@ var handlers = map[string]resourceHandler{}

func RegisterRoutes(group *gin.RouterGroup) {
handlers = map[string]resourceHandler{
"pods": NewPodHandler(),
"namespaces": NewGenericResourceHandler[*corev1.Namespace, *corev1.NamespaceList]("namespaces", true, false),
"nodes": NewNodeHandler(),
"services": NewGenericResourceHandler[*corev1.Service, *corev1.ServiceList]("services", false, true),
"endpoints": NewGenericResourceHandler[*corev1.Endpoints, *corev1.EndpointsList]("endpoints", false, false),
"endpointslices": NewGenericResourceHandler[*discoveryv1.EndpointSlice, *discoveryv1.EndpointSliceList]("endpointslices", false, false),
"configmaps": NewGenericResourceHandler[*corev1.ConfigMap, *corev1.ConfigMapList]("configmaps", false, true),
"secrets": NewGenericResourceHandler[*corev1.Secret, *corev1.SecretList]("secrets", false, true),
"persistentvolumes": NewGenericResourceHandler[*corev1.PersistentVolume, *corev1.PersistentVolumeList]("persistentvolumes", true, true),
"persistentvolumeclaims": NewGenericResourceHandler[*corev1.PersistentVolumeClaim, *corev1.PersistentVolumeClaimList]("persistentvolumeclaims", false, true),
"serviceaccounts": NewGenericResourceHandler[*corev1.ServiceAccount, *corev1.ServiceAccountList]("serviceaccounts", false, false),
"crds": NewGenericResourceHandler[*apiextensionsv1.CustomResourceDefinition, *apiextensionsv1.CustomResourceDefinitionList]("crds", true, false),
"events": NewEventHandler(),
"deployments": NewDeploymentHandler(),
"replicasets": NewGenericResourceHandler[*appsv1.ReplicaSet, *appsv1.ReplicaSetList]("replicasets", false, false),
"statefulsets": NewGenericResourceHandler[*appsv1.StatefulSet, *appsv1.StatefulSetList]("statefulsets", false, false),
"daemonsets": NewGenericResourceHandler[*appsv1.DaemonSet, *appsv1.DaemonSetList]("daemonsets", false, true),
"jobs": NewGenericResourceHandler[*batchv1.Job, *batchv1.JobList]("jobs", false, false),
"cronjobs": NewGenericResourceHandler[*batchv1.CronJob, *batchv1.CronJobList]("cronjobs", false, false),
"ingresses": NewGenericResourceHandler[*networkingv1.Ingress, *networkingv1.IngressList]("ingresses", false, false),
"storageclasses": NewGenericResourceHandler[*storagev1.StorageClass, *storagev1.StorageClassList]("storageclasses", true, false),
"roles": NewGenericResourceHandler[*rbacv1.Role, *rbacv1.RoleList]("roles", false, false),
"rolebindings": NewGenericResourceHandler[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]("rolebindings", false, false),
"clusterroles": NewGenericResourceHandler[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]("clusterroles", true, false),
"clusterrolebindings": NewGenericResourceHandler[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]("clusterrolebindings", true, false),
"podmetrics": NewGenericResourceHandler[*metricsv1.PodMetrics, *metricsv1.PodMetricsList]("metrics.k8s.io", false, false),
"nodemetrics": NewGenericResourceHandler[*metricsv1.NodeMetrics, *metricsv1.NodeMetricsList]("metrics.k8s.io", false, false),
"gatewssays": NewGenericResourceHandler[*gatewayapiv1.Gateway, *gatewayapiv1.GatewayList]("gateways", false, true),
"httproutes": NewGenericResourceHandler[*gatewayapiv1.HTTPRoute, *gatewayapiv1.HTTPRouteList]("httproutes", false, true),
"pods": NewPodHandler(),
"namespaces": NewGenericResourceHandler[*corev1.Namespace, *corev1.NamespaceList]("namespaces", true, false),
"nodes": NewNodeHandler(),
"services": NewGenericResourceHandler[*corev1.Service, *corev1.ServiceList]("services", false, true),
"endpoints": NewGenericResourceHandler[*corev1.Endpoints, *corev1.EndpointsList]("endpoints", false, false),
"endpointslices": NewGenericResourceHandler[*discoveryv1.EndpointSlice, *discoveryv1.EndpointSliceList]("endpointslices", false, false),
"configmaps": NewGenericResourceHandler[*corev1.ConfigMap, *corev1.ConfigMapList]("configmaps", false, true),
"secrets": NewGenericResourceHandler[*corev1.Secret, *corev1.SecretList]("secrets", false, true),
"persistentvolumes": NewGenericResourceHandler[*corev1.PersistentVolume, *corev1.PersistentVolumeList]("persistentvolumes", true, true),
"persistentvolumeclaims": NewGenericResourceHandler[*corev1.PersistentVolumeClaim, *corev1.PersistentVolumeClaimList]("persistentvolumeclaims", false, true),
"serviceaccounts": NewGenericResourceHandler[*corev1.ServiceAccount, *corev1.ServiceAccountList]("serviceaccounts", false, false),
"crds": NewGenericResourceHandler[*apiextensionsv1.CustomResourceDefinition, *apiextensionsv1.CustomResourceDefinitionList]("crds", true, false),
"events": NewEventHandler(),
"deployments": NewDeploymentHandler(),
"replicasets": NewGenericResourceHandler[*appsv1.ReplicaSet, *appsv1.ReplicaSetList]("replicasets", false, false),
"statefulsets": NewGenericResourceHandler[*appsv1.StatefulSet, *appsv1.StatefulSetList]("statefulsets", false, false),
"daemonsets": NewGenericResourceHandler[*appsv1.DaemonSet, *appsv1.DaemonSetList]("daemonsets", false, true),
"jobs": NewGenericResourceHandler[*batchv1.Job, *batchv1.JobList]("jobs", false, false),
"cronjobs": NewGenericResourceHandler[*batchv1.CronJob, *batchv1.CronJobList]("cronjobs", false, false),
"ingresses": NewGenericResourceHandler[*networkingv1.Ingress, *networkingv1.IngressList]("ingresses", false, false),
"storageclasses": NewGenericResourceHandler[*storagev1.StorageClass, *storagev1.StorageClassList]("storageclasses", true, false),
"roles": NewGenericResourceHandler[*rbacv1.Role, *rbacv1.RoleList]("roles", false, false),
"rolebindings": NewGenericResourceHandler[*rbacv1.RoleBinding, *rbacv1.RoleBindingList]("rolebindings", false, false),
"clusterroles": NewGenericResourceHandler[*rbacv1.ClusterRole, *rbacv1.ClusterRoleList]("clusterroles", true, false),
"clusterrolebindings": NewGenericResourceHandler[*rbacv1.ClusterRoleBinding, *rbacv1.ClusterRoleBindingList]("clusterrolebindings", true, false),
"podmetrics": NewGenericResourceHandler[*metricsv1.PodMetrics, *metricsv1.PodMetricsList]("metrics.k8s.io", false, false),
"nodemetrics": NewGenericResourceHandler[*metricsv1.NodeMetrics, *metricsv1.NodeMetricsList]("metrics.k8s.io", false, false),
"gatewssays": NewGenericResourceHandler[*gatewayapiv1.Gateway, *gatewayapiv1.GatewayList]("gateways", false, true),
"httproutes": NewGenericResourceHandler[*gatewayapiv1.HTTPRoute, *gatewayapiv1.HTTPRouteList]("httproutes", false, true),
"horizontalpodautoscalers": NewGenericResourceHandler[*autoscalingv2.HorizontalPodAutoscaler, *autoscalingv2.HorizontalPodAutoscalerList]("horizontalpodautoscalers", false, true),
}

for name, handler := range handlers {
Expand All @@ -90,7 +92,7 @@ func RegisterRoutes(group *gin.RouterGroup) {
}

// Register related resources route for supported resource types
supportedRelatedResourceTypes := []string{"pods", "deployments", "statefulsets", "daemonsets", "configmaps", "secrets", "persistentvolumeclaims", "httproutes"}
supportedRelatedResourceTypes := []string{"pods", "deployments", "statefulsets", "daemonsets", "configmaps", "secrets", "persistentvolumeclaims", "httproutes", "horizontalpodautoscalers"}
for _, resourceType := range supportedRelatedResourceTypes {
if handler, exists := handlers[resourceType]; exists && !handler.IsClusterScoped() {
g := group.Group("/" + resourceType)
Expand Down
15 changes: 15 additions & 0 deletions pkg/handlers/resources/related_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/zxh326/kite/pkg/common"
"github.com/zxh326/kite/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -247,6 +248,8 @@ func GetRelatedResources(c *gin.Context) {
}
case *gatewayapiv1.HTTPRoute:
result = getHTTPRouteRelatedResouces(res, namespace)
case *autoscalingv2.HorizontalPodAutoscaler:
result = getAutoScalingRelatedResources(res, namespace)
}

if podSpec != nil && selector != nil {
Expand Down Expand Up @@ -340,3 +343,15 @@ func getHTTPRouteRelatedResouces(res *gatewayapiv1.HTTPRoute, namespace string)
}
return result
}

func getAutoScalingRelatedResources(res *autoscalingv2.HorizontalPodAutoscaler, namespace string) []common.RelatedResource {
var result []common.RelatedResource
scaleTarget := res.Spec.ScaleTargetRef
result = append(result, common.RelatedResource{
Type: strings.ToLower(scaleTarget.Kind) + "s",
APIVersion: scaleTarget.APIVersion,
Name: scaleTarget.Name,
Namespace: namespace,
})
return result
}
1 change: 1 addition & 0 deletions ui/src/components/dynamic-breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function DynamicBreadcrumb() {
pvcs: t('sidebar.short.pvcs'),
crds: t('nav.crds'),
crs: t('nav.customResources'),
horizontalpodautoscalers: t('nav.horizontalpodautoscalers'),
}

// Helper function to create breadcrumb item
Expand Down
5 changes: 5 additions & 0 deletions ui/src/components/global-search.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
import {
IconArrowsHorizontal,
IconBox,
IconBoxMultiple,
IconLoadBalancer,
Expand Down Expand Up @@ -64,6 +65,10 @@ const RESOURCE_CONFIG: Record<
label: 'nav.daemonsets',
icon: IconTopologyBus,
},
horizontalpodautoscalers: {
label: 'nav.horizontalpodautoscalers',
icon: IconArrowsHorizontal,
},
}

interface GlobalSearchProps {
Expand Down
7 changes: 7 additions & 0 deletions ui/src/contexts/sidebar-config-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import * as React from 'react'
import {
Icon,
IconArrowsHorizontal,
IconBell,
IconBox,
IconBoxMultiple,
Expand Down Expand Up @@ -68,6 +69,7 @@ const iconMap = {
IconServer2,
IconBell,
IconCode,
IconArrowsHorizontal,
}

const getIconName = (iconComponent: React.ComponentType): string => {
Expand Down Expand Up @@ -159,6 +161,11 @@ export const SidebarConfigProvider: React.FC<SidebarConfigProviderProps> = ({
'sidebar.groups.config': [
{ titleKey: 'nav.configMaps', url: '/configmaps', icon: IconMap },
{ titleKey: 'nav.secrets', url: '/secrets', icon: IconLock },
{
titleKey: 'nav.horizontalpodautoscalers',
url: '/horizontalpodautoscalers',
icon: IconArrowsHorizontal,
},
],
'sidebar.groups.security': [
{
Expand Down
3 changes: 2 additions & 1 deletion ui/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"roles": "Roles",
"rolebindings": "Role Bindings",
"clusterroles": "Cluster Roles",
"clusterrolebindings": "Cluster Role Bindings"
"clusterrolebindings": "Cluster Role Bindings",
"horizontalpodautoscalers": "HPA"
},
"overview": {
"title": "Overview",
Expand Down
3 changes: 2 additions & 1 deletion ui/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
"roles": "角色",
"rolebindings": "角色绑定",
"clusterroles": "集群角色",
"clusterrolebindings": "集群角色绑定"
"clusterrolebindings": "集群角色绑定",
"horizontalpodautoscalers": "Pod 水平自动扩缩"
},
"sidebar": {
"groups": {
Expand Down
127 changes: 127 additions & 0 deletions ui/src/pages/horizontalpodautoscaler-list-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useCallback, useMemo } from 'react'
import { createColumnHelper } from '@tanstack/react-table'
import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v2'
import { Link } from 'react-router-dom'

import { formatDate } from '@/lib/utils'
import { ResourceTable } from '@/components/resource-table'

function getHpaTargetInfo(hpa: HorizontalPodAutoscaler): string {
if (!hpa.spec?.scaleTargetRef) {
return '-'
}
const { kind, name } = hpa.spec.scaleTargetRef
return `${kind}/${name}`
}

function getCurrentReplicas(hpa: HorizontalPodAutoscaler): number {
return hpa.status?.currentReplicas || 0
}

function getMetricUtilization(hpa: HorizontalPodAutoscaler): string {
if (!hpa.status?.currentMetrics || hpa.status.currentMetrics.length === 0) {
return '-'
}

const metrics = hpa.status.currentMetrics
const results: string[] = []

metrics.forEach((metric) => {
if ('resource' in metric && metric.resource) {
const current = metric.resource.current?.averageUtilization || 0
const target =
hpa.spec?.metrics?.find(
(m) => 'resource' in m && m.resource?.name === metric.resource?.name
)?.resource?.target?.averageUtilization || 0
results.push(`${metric.resource.name}: ${current}% / ${target}%`)
} else if (metric.type === 'Pods') {
results.push('Pods metric')
} else if (metric.type === 'Object') {
results.push('Object metric')
} else if (metric.type === 'External') {
results.push('External metric')
}
})

return results.join(', ')
}

export function HorizontalPodAutoscalerListPage() {
const columnHelper = createColumnHelper<HorizontalPodAutoscaler>()

const columns = useMemo(
() => [
columnHelper.accessor('metadata.name', {
header: 'Name',
cell: ({ row }) => (
<div className="font-medium text-blue-500 hover:underline">
<Link
to={`/horizontalpodautoscalers/${row.original.metadata!.namespace}/${
row.original.metadata!.name
}`}
>
{row.original.metadata!.name}
</Link>
</div>
),
}),
columnHelper.accessor((row) => getHpaTargetInfo(row), {
header: 'Target',
cell: ({ getValue }) => getValue(),
}),
columnHelper.accessor((row) => row.spec?.minReplicas, {
id: 'minReplicas',
header: 'Min Pods',
cell: ({ getValue }) => getValue() || '-',
}),
columnHelper.accessor((row) => row.spec?.maxReplicas, {
id: 'maxReplicas',
header: 'Max Pods',
cell: ({ getValue }) => getValue() || '-',
}),
columnHelper.accessor((row) => getCurrentReplicas(row), {
id: 'currentReplicas',
header: 'Current Pods',
cell: ({ getValue }) => getValue(),
}),
columnHelper.accessor((row) => getMetricUtilization(row), {
id: 'metrics',
header: 'Metrics',
cell: ({ getValue }) => (
<span className="text-muted-foreground text-sm">{getValue()}</span>
),
}),
columnHelper.accessor('metadata.creationTimestamp', {
header: 'Created',
cell: ({ getValue }) => {
const dateStr = formatDate(getValue() || '')

return (
<span className="text-muted-foreground text-sm">{dateStr}</span>
)
},
}),
],
[columnHelper]
)

const horizontalPodAutoscalerSearchFilter = useCallback(
(hpa: HorizontalPodAutoscaler, query: string) => {
const queryLower = query.toLowerCase()
return (
hpa.metadata!.name!.toLowerCase().includes(queryLower) ||
(hpa.metadata!.namespace?.toLowerCase() || '').includes(queryLower) ||
getHpaTargetInfo(hpa).toLowerCase().includes(queryLower)
)
},
[]
)

return (
<ResourceTable
resourceName="HorizontalPodAutoscalers"
columns={columns}
searchQueryFilter={horizontalPodAutoscalerSearchFilter}
/>
)
}
3 changes: 3 additions & 0 deletions ui/src/pages/resource-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CRDListPage } from './crd-list-page'
import { DaemonSetListPage } from './daemonset-list-page'
import { DeploymentListPage } from './deployment-list-page'
import { GatewayListPage } from './gateway-list-page'
import { HorizontalPodAutoscalerListPage } from './horizontalpodautoscaler-list-page'
import { HTTPRouteListPage } from './httproute-list-page'
import { IngressListPage } from './ingress-list-page'
import { JobListPage } from './job-list-page'
Expand Down Expand Up @@ -53,6 +54,8 @@ export function ResourceList() {
return <GatewayListPage />
case 'httproutes':
return <HTTPRouteListPage />
case 'horizontalpodautoscalers':
return <HorizontalPodAutoscalerListPage />
default:
return <SimpleListPage resourceType={resource as ResourceType} />
}
Expand Down
7 changes: 7 additions & 0 deletions ui/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
StatefulSet,
StatefulSetList,
} from 'kubernetes-types/apps/v1'
import {
HorizontalPodAutoscaler,
HorizontalPodAutoscalerList,
} from 'kubernetes-types/autoscaling/v2'
import { CronJob, CronJobList, Job, JobList } from 'kubernetes-types/batch/v1'
import {
ConfigMap,
Expand Down Expand Up @@ -107,6 +111,7 @@ export type ResourceType =
| 'rolebindings'
| 'clusterroles'
| 'clusterrolebindings'
| 'horizontalpodautoscalers'

export const clusterScopeResources: ResourceType[] = [
'crds',
Expand Down Expand Up @@ -163,6 +168,7 @@ export interface ResourcesTypeMap {
rolebindings: RoleBindingList
clusterroles: ClusterRoleList
clusterrolebindings: ClusterRoleBindingList
horizontalpodautoscalers: HorizontalPodAutoscalerList
}

export interface PodMetrics {
Expand Down Expand Up @@ -227,6 +233,7 @@ export interface ResourceTypeMap {
rolebindings: RoleBinding
clusterroles: ClusterRole
clusterrolebindings: ClusterRoleBinding
horizontalpodautoscalers: HorizontalPodAutoscaler
}

export interface RecentEvent {
Expand Down