dynamic.go•21.8 kB
package dynamic
import (
"context"
"fmt"
"slices"
"strings"
"github.com/duke-git/lancet/v2/slice"
"github.com/gin-gonic/gin"
utils2 "github.com/weibaohui/k8m/pkg/comm/utils"
"github.com/weibaohui/k8m/pkg/comm/utils/amis"
"github.com/weibaohui/k8m/pkg/service"
"github.com/weibaohui/kom/kom"
"github.com/weibaohui/kom/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog/v2"
"sigs.k8s.io/yaml"
)
type ActionController struct{}
func RegisterActionRoutes(api *gin.RouterGroup) {
ctrl := &ActionController{}
api.GET("/:kind/group/:group/version/:version/ns/:ns/name/:name", ctrl.Fetch) // CRD
api.GET("/:kind/group/:group/version/:version/ns/:ns/name/:name/json", ctrl.FetchJson) // CRD
api.GET("/:kind/group/:group/version/:version/ns/:ns/name/:name/event", ctrl.Event) // CRD
api.GET("/:kind/group/:group/version/:version/ns/:ns/name/:name/hpa", ctrl.HPA) // CRD
api.POST("/:kind/group/:group/version/:version/ns/:ns/name/:name/scale/replica/:replica", ctrl.Scale) // CRD
api.POST("/:kind/group/:group/version/:version/remove/ns/:ns/name/:name", ctrl.Remove) // CRD
api.POST("/:kind/group/:group/version/:version/batch/remove", ctrl.BatchRemove) // CRD
api.POST("/:kind/group/:group/version/:version/force_remove", ctrl.BatchForceRemove) // CRD
api.POST("/:kind/group/:group/version/:version/update/ns/:ns/name/:name", ctrl.Save) // CRD // CRD
api.POST("/:kind/group/:group/version/:version/describe/ns/:ns/name/:name", ctrl.Describe) // CRD
api.POST("/:kind/group/:group/version/:version/list/ns/:ns", ctrl.List) // CRD
api.POST("/:kind/group/:group/version/:version/list/ns/", ctrl.List) // CRD
api.POST("/:kind/group/:group/version/:version/list", ctrl.List)
}
// @Summary 获取资源列表
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/list/ns/{ns} [post]
func (ac *ActionController) List(c *gin.Context) {
ns := c.Param("ns")
group := c.Param("group")
kind := c.Param("kind")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
var nsList []string
if strings.Contains(ns, ",") {
nsList = strings.Split(ns, ",")
} else {
nsList = []string{ns}
}
// 用于存储 JSON 数据的 map
var jsonData map[string]any
if err = c.ShouldBindJSON(&jsonData); err != nil {
amis.WriteJsonError(c, err)
return
}
var total int64
var list []*unstructured.Unstructured
sql := kom.Cluster(selectedCluster).WithContext(ctx).
RemoveManagedFields().
Namespace(nsList...).
GVK(group, version, kind)
if uniqueLabels, ok := jsonData["unique_labels"]; ok {
if uniqueLabels != "" {
delete(jsonData, "unique_labels")
sql = sql.WithLabelSelector(uniqueLabels.(string))
}
}
// 处理查询条件
queryConditions := parseNestedJSON("", jsonData)
queryConditions = slice.Filter(queryConditions, func(index int, item string) bool {
return !strings.HasSuffix(item, "=")
})
// 检查jsonData中metadata.namespace是否在nsList中,如果不在,则增加到nsList中
if namespaceStr := getNestedStringFromJSON(jsonData, "metadata.namespace"); namespaceStr != "" {
// 检查namespaceStr是否在nsList中
namespaceInList := slices.Contains(nsList, namespaceStr)
if !namespaceInList {
// 如果命名空间不在范围内,自动添加到nsList中
nsList = append(nsList, namespaceStr)
klog.V(8).Infof("查询条件中的命名空间 '%s' 已自动添加到查询范围中,当前范围: [%s]", namespaceStr, strings.Join(nsList, ","))
// 需要重新设置Namespace范围
sql = sql.Namespace(nsList...)
}
}
if len(queryConditions) > 0 {
queryString := strings.Join(queryConditions, " and ")
klog.V(6).Infof("sql string =%s", queryString)
sql = sql.Where(queryString)
}
// 处理OrderBy,默认asc
// orderBy = 字段
// orderDir = asc/desc/空
orderBy, orderByOK := jsonData["orderBy"].(string)
orderDir, orderDirOK := jsonData["orderDir"].(string)
if orderByOK {
if orderDirOK {
sql = sql.Order(fmt.Sprintf("%s %s", orderBy, orderDir))
} else {
sql = sql.Order(fmt.Sprintf("%s asc", orderBy))
}
}
// 取出 page 和 perPage
// 取出 page 和 perPage
page, pageOK := jsonData["page"].(float64) // JSON 数字会解析为 float64
perPage, perPageOK := jsonData["perPage"].(float64)
if pageOK {
sql = sql.Limit(int(perPage))
}
if perPageOK {
sql = sql.Offset((int(page) - 1) * int(perPage))
}
// 执行SQL
err = sql.
FillTotalCount(&total).
List(&list).Error
list = ac.fillList(selectedCluster, kind, list)
amis.WriteJsonListTotalWithError(c, total, list, err)
}
// fillList 定制填充list []*unstructured.Unstructured列表
func (ac *ActionController) fillList(selectedCluster string, kind string, list []*unstructured.Unstructured) []*unstructured.Unstructured {
switch kind {
case "Node":
if service.ClusterService().GetNodeStatusAggregated(selectedCluster) {
// 已缓存聚合状态,可以填充
for i := range list {
item := list[i]
service.NodeService().SetIPUsage(selectedCluster, item)
service.NodeService().SetPodCount(selectedCluster, item)
service.NodeService().SetAllocatedStatus(selectedCluster, item)
}
}
case "Pod":
if service.ClusterService().GetPodStatusAggregated(selectedCluster) {
// 已缓存聚合状态,可以填充
for i := range list {
item := list[i]
service.PodService().SetAllocatedStatusOnPod(selectedCluster, item)
}
}
case "Namespace":
if service.ClusterService().GetPodStatusAggregated(selectedCluster) {
// 已缓存聚合状态,可以填充
for i := range list {
item := list[i]
service.PodService().SetStatusCountOnNamespace(selectedCluster, item)
}
}
case "StorageClass":
if service.ClusterService().GetPVCStatusAggregated(selectedCluster) {
// 已缓存聚合状态,可以填充
for i := range list {
item := list[i]
service.StorageClassService().SetPVCCount(selectedCluster, item)
}
}
if service.ClusterService().GetPVStatusAggregated(selectedCluster) {
// 已缓存聚合状态,可以填充
for i := range list {
item := list[i]
service.StorageClassService().SetPVCount(selectedCluster, item)
}
}
case "IngressClass":
if service.ClusterService().GetIngressStatusAggregated(selectedCluster) {
// 已缓存聚合状态,可以填充
for i := range list {
item := list[i]
service.IngressClassService().SetIngressCount(selectedCluster, item)
}
}
}
return list
}
// @Summary 获取资源事件
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/ns/{ns}/name/{name}/event [get]
func (ac *ActionController) Event(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
apiVersion := version
if group != "" {
apiVersion = fmt.Sprintf("%s/%s", group, version)
}
fieldSelector := fmt.Sprintf("regarding.apiVersion=%s,regarding.kind=%s,regarding.name=%s,regarding.namespace=%s", apiVersion, kind, name, ns)
var eventList []*unstructured.Unstructured
err = kom.Cluster(selectedCluster).
WithContext(ctx).
RemoveManagedFields().
Namespace(ns).
GVK("events.k8s.io", "v1", "Event").
List(&eventList, metav1.ListOptions{
FieldSelector: fieldSelector,
}).Error
amis.WriteJsonListWithError(c, eventList, err)
}
// @Summary 获取资源YAML
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/ns/{ns}/name/{name} [get]
func (ac *ActionController) Fetch(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
var obj *unstructured.Unstructured
err = kom.Cluster(selectedCluster).WithContext(ctx).RemoveManagedFields().Name(name).Namespace(ns).CRD(group, version, kind).Get(&obj).Error
if err != nil {
amis.WriteJsonError(c, err)
return
}
yamlStr, err := utils.ConvertUnstructuredToYAML(obj)
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonData(c, gin.H{
"yaml": yamlStr,
})
}
// @Summary 获取资源JSON
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/ns/{ns}/name/{name}/json [get]
func (ac *ActionController) FetchJson(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
var obj *unstructured.Unstructured
err = kom.Cluster(selectedCluster).WithContext(ctx).RemoveManagedFields().Name(name).Namespace(ns).CRD(group, version, kind).Get(&obj).Error
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonData(c, obj)
}
// @Summary 删除单个资源
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/remove/ns/{ns}/name/{name} [post]
func (ac *ActionController) Remove(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
err = ac.removeSingle(ctx, selectedCluster, kind, group, version, ns, name, false)
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonOK(c)
}
func (ac *ActionController) removeSingle(ctx context.Context, selectedCluster, kind, group, version, ns, name string, force bool) error {
if force {
return kom.Cluster(selectedCluster).WithContext(ctx).Name(name).Namespace(ns).CRD(group, version, kind).ForceDelete().Error
}
return kom.Cluster(selectedCluster).WithContext(ctx).Name(name).Namespace(ns).CRD(group, version, kind).Delete().Error
}
// NamesPayload 定义结构体以匹配批量删除 JSON 结构
type NamesPayload struct {
Names []string `json:"names"`
}
// @Summary 批量删除资源
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param name_list body []string true "资源名称列表"
// @Param ns_list body []string true "命名空间列表"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/batch/remove [post]
func (ac *ActionController) BatchRemove(c *gin.Context) {
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
var req struct {
Names []string `json:"name_list"`
Namespaces []string `json:"ns_list"`
}
if err = c.ShouldBindJSON(&req); err != nil {
amis.WriteJsonError(c, err)
return
}
for i := 0; i < len(req.Names); i++ {
name := req.Names[i]
ns := req.Namespaces[i]
x := ac.removeSingle(ctx, selectedCluster, kind, group, version, ns, name, false)
if x != nil {
klog.V(6).Infof("batch remove %s error %s/%s %v", kind, ns, name, x)
err = x
}
}
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonOK(c)
}
// @Summary 批量强制删除资源
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param name_list body []string true "资源名称列表"
// @Param ns_list body []string true "命名空间列表"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/force_remove [post]
func (ac *ActionController) BatchForceRemove(c *gin.Context) {
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
var req struct {
Names []string `json:"name_list"`
Namespaces []string `json:"ns_list"`
}
if err = c.ShouldBindJSON(&req); err != nil {
amis.WriteJsonError(c, err)
return
}
for i := 0; i < len(req.Names); i++ {
name := req.Names[i]
ns := req.Namespaces[i]
x := ac.removeSingle(ctx, selectedCluster, kind, group, version, ns, name, true)
if x != nil {
klog.V(6).Infof("batch force remove %s error %s/%s %v", kind, ns, name, x)
err = x
}
}
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonOK(c)
}
// @Summary 更新资源
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Param yaml body string true "资源YAML内容"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/update/ns/{ns}/name/{name} [post]
func (ac *ActionController) Save(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
var req yamlRequest
if err = c.ShouldBindJSON(&req); err != nil {
amis.WriteJsonError(c, err)
return
}
yamlStr := req.Yaml
// 解析 Yaml 到 Unstructured 对象
obj := &unstructured.Unstructured{}
var raw map[string]any
if err = yaml.Unmarshal([]byte(yamlStr), &raw); err != nil {
amis.WriteJsonError(c, err)
return
}
obj.Object = raw
obj.SetName(name)
obj.SetNamespace(ns)
err = kom.Cluster(selectedCluster).WithContext(ctx).Name(name).Namespace(ns).CRD(group, version, kind).Update(&obj).Error
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonOK(c)
}
// @Summary 描述资源
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/describe/ns/{ns}/name/{name} [post]
func (ac *ActionController) Describe(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
var result []byte
err = kom.Cluster(selectedCluster).WithContext(ctx).Name(name).Namespace(ns).CRD(group, version, kind).Describe(&result).Error
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonData(c, string(result))
}
// @Summary 扩缩容资源
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Param replica path string true "副本数"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/ns/{ns}/name/{name}/scale/replica/{replica} [post]
func (ac *ActionController) Scale(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
replica := c.Param("replica")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
r := utils2.ToInt32(replica)
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
err = kom.Cluster(selectedCluster).WithContext(ctx).
CRD(group, version, kind).
Namespace(ns).Name(name).
Ctl().Scaler().Scale(r)
amis.WriteJsonErrorOrOK(c, err)
}
// @Summary 获取资源HPA信息
// @Security BearerAuth
// @Param cluster query string true "集群名称"
// @Param kind path string true "资源类型"
// @Param group path string true "资源组"
// @Param version path string true "资源版本"
// @Param ns path string true "命名空间"
// @Param name path string true "资源名称"
// @Success 200 {object} string
// @Router /k8s/cluster/{cluster}/{kind}/group/{group}/version/{version}/ns/{ns}/name/{name}/hpa [get]
func (ac *ActionController) HPA(c *gin.Context) {
ns := c.Param("ns")
name := c.Param("name")
kind := c.Param("kind")
group := c.Param("group")
version := c.Param("version")
ctx := amis.GetContextWithUser(c)
selectedCluster, err := amis.GetSelectedCluster(c)
if err != nil {
amis.WriteJsonError(c, err)
return
}
hpa, err := kom.Cluster(selectedCluster).WithContext(ctx).
CRD(group, version, kind).Namespace(ns).Name(name).
Ctl().CRD().HPAList()
if err != nil {
amis.WriteJsonError(c, err)
return
}
amis.WriteJsonData(c, hpa)
}
// getNestedStringFromJSON 从嵌套的JSON数据中获取指定路径的字符串值
// path参数使用点号分隔,例如: "metadata.namespace", "spec.replicas"
// 如果路径不存在或值不是字符串类型,返回空字符串
func getNestedStringFromJSON(data map[string]any, path string) string {
if data == nil || path == "" {
return ""
}
// 按点号分割路径
keys := strings.Split(path, ".")
current := data
// 逐层向下查找
for i, key := range keys {
value, exists := current[key]
if !exists {
return ""
}
// 如果是最后一层,尝试转换为字符串
if i == len(keys)-1 {
if str, ok := value.(string); ok {
return str
}
return ""
}
// 如果不是最后一层,必须是map类型才能继续
if nextMap, ok := value.(map[string]any); ok {
current = nextMap
} else {
return ""
}
}
return ""
}
// 递归解析 JSON 数据
//
// queryConditions := parseNestedJSON("", jsonData)
//
// // 示例 JSON 数据
//
// jsonData := map[string]any{
// "page": 1,
// "metadata": map[string]any{
// "name": "nginx",
// },
// "status": map[string]any{
// "phase": "Running",
// },
// "perPage": 10,
// }
// queryString := strings.Join(queryConditions, "&")
// 输出: page=1&metadata.name=nginx&status.phase=Running&perPage=10
func parseNestedJSON(prefix string, data map[string]any) []string {
var result []string
for key, value := range data {
// 拼接当前路径
currentKey := key
if prefix != "" {
currentKey = prefix + "." + key
}
// 分页参数跳过
// ns name 已经单独在kom调用链中单独设定,不需要设置到where条件中
ignoreKeys := []string{"page", "perPage", "pageDir", "orderDir", "orderBy", "keywords", "ns", "name"}
if slice.Contain(ignoreKeys, currentKey) {
continue
}
switch v := value.(type) {
case map[string]any:
// 递归解析嵌套对象
result = append(result, parseNestedJSON(currentKey, v)...)
default:
// 添加键值对
if v == "" {
// 没有值跳过
continue
}
result = append(result, fmt.Sprintf("`%s` like '%%%v%%'", currentKey, v))
}
}
return result
}