- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂 - i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏 - 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条 - 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器 - UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正 - 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简 - 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
106 lines
3.2 KiB
Go
106 lines
3.2 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
"lokistack/models"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type ClusterInfo struct {
|
|
Version string `json:"version"`
|
|
Nodes []NodeInfo `json:"nodes"`
|
|
Namespaces []string `json:"namespaces"`
|
|
Contexts []string `json:"contexts"`
|
|
CurrentCtx string `json:"currentContext"`
|
|
}
|
|
|
|
type NodeInfo struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Roles []string `json:"roles"`
|
|
Version string `json:"version"`
|
|
OS string `json:"os"`
|
|
Arch string `json:"arch"`
|
|
InternalIP string `json:"internalIP"`
|
|
Labels map[string]string `json:"labels"`
|
|
}
|
|
|
|
// GetClusterInfo 从 API Server 读取集群信息,context 列表由 kubeconfig 本地解析。
|
|
func GetClusterInfo(ctx context.Context, cluster *models.Cluster) (*ClusterInfo, error) {
|
|
client, err := clusterClient(cluster, 30*time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info := &ClusterInfo{Nodes: []NodeInfo{}, Namespaces: []string{}, Contexts: []string{}}
|
|
if version, err := client.Discovery().ServerVersion(); err == nil {
|
|
info.Version = version.GitVersion
|
|
}
|
|
nodes, err := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
|
|
if err != nil && !apierrors.IsForbidden(err) {
|
|
return nil, fmt.Errorf("获取节点列表失败: %w", err)
|
|
}
|
|
// 命名空间级 Token 通常没有 nodes 权限:跳过节点信息,不影响集群可用性
|
|
var nodeItems []corev1.Node
|
|
if nodes != nil {
|
|
nodeItems = nodes.Items
|
|
}
|
|
for _, item := range nodeItems {
|
|
status := "Unknown"
|
|
for _, cond := range item.Status.Conditions {
|
|
if cond.Type == "Ready" {
|
|
if cond.Status == "True" {
|
|
status = "Ready"
|
|
} else {
|
|
status = "NotReady"
|
|
}
|
|
break
|
|
}
|
|
}
|
|
roles := []string{}
|
|
for label := range item.Labels {
|
|
if strings.HasPrefix(label, "node-role.kubernetes.io/") {
|
|
roles = append(roles, strings.TrimPrefix(label, "node-role.kubernetes.io/"))
|
|
}
|
|
}
|
|
if len(roles) == 0 {
|
|
roles = []string{"worker"}
|
|
}
|
|
sort.Strings(roles)
|
|
ip := ""
|
|
for _, address := range item.Status.Addresses {
|
|
if address.Type == "InternalIP" {
|
|
ip = address.Address
|
|
break
|
|
}
|
|
}
|
|
info.Nodes = append(info.Nodes, NodeInfo{Name: item.Name, Status: status, Roles: roles, Version: item.Status.NodeInfo.KubeletVersion, OS: item.Status.NodeInfo.OperatingSystem, Arch: item.Status.NodeInfo.Architecture, InternalIP: ip, Labels: item.Labels})
|
|
}
|
|
if namespaces, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}); err == nil {
|
|
for _, item := range namespaces.Items {
|
|
info.Namespaces = append(info.Namespaces, item.Name)
|
|
}
|
|
sort.Strings(info.Namespaces)
|
|
}
|
|
source, err := resolveKubeconfig(cluster)
|
|
if err == nil {
|
|
if raw, err := clientcmd.Load([]byte(source)); err == nil {
|
|
for name := range raw.Contexts {
|
|
info.Contexts = append(info.Contexts, name)
|
|
}
|
|
sort.Strings(info.Contexts)
|
|
info.CurrentCtx = raw.CurrentContext
|
|
if cluster.Context != "" {
|
|
info.CurrentCtx = cluster.Context
|
|
}
|
|
}
|
|
}
|
|
return info, nil
|
|
}
|