├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── apis ├── cluster.go ├── common.go ├── configmap.go ├── event.go ├── namespace.go ├── pod.go ├── svc.go └── terminal.go ├── config ├── .kube │ └── default │ │ └── config ├── application.yaml └── config.go ├── docker-deploy.sh ├── go.mod ├── k8s-deploy.sh ├── k8s-kubernetes-admin-backend.yaml ├── main.go ├── middleware ├── Cors.go └── Monitor.go ├── proto ├── Cluster.go ├── apiresources.go ├── configmap.go ├── container.go ├── extraclusterinfo.go ├── namespace.go ├── node.go ├── pod.go ├── result.go └── svc.go ├── router └── routes.go ├── service ├── cluster.go ├── common.go ├── configmap.go ├── event.go ├── informers.go ├── namespace.go ├── pod.go └── svc.go ├── terminal ├── terminal_pod.go └── terminal_ssh.go └── test └── cluster_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM loads/alpine:3.8 2 | LABEL maintainer="旅店老板" 3 | # 设置固定的项目路径 4 | ENV WORKDIR /var/www/kubernetes-admin-backend 5 | 6 | # 添加应用可执行文件,并设置执行权限 7 | ADD ./bin/linux_amd64/main $WORKDIR/main 8 | RUN chmod +x $WORKDIR/main 9 | 10 | ADD config $WORKDIR/config 11 | #ADD public $WORKDIR/public 12 | #ADD template $WORKDIR/template 13 | 14 | WORKDIR $WORKDIR 15 | CMD ./main 16 | 17 | # 18 | 19 | #gf docker -t qc-image:v2 或 docker build -t qc-image:v2 . 20 | #SET CGO_ENABLED=0 21 | #SET GOOS=linux 22 | #SET GOARCH=amd64 23 | #go build main.go 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ldlb9527 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/badge/-kubernetes--admin--backend-green) 2 | # k8s可视化管理平台 3 | ## 1.项目结构介绍 4 | * `apis`: 控制器,接口访问的入口,文件大多以k8s资源命名 5 | * `config`: 配置文件,程序端口和k8s的config配置 6 | * `middleware`:中间件,配置跨域访问和prometheus监控 7 | * `proto`: 返回给前端的实体,类似于VO对象,Cluster.go用于配置多集群管理 8 | * `router`: 路由层,路由分组和匹配,与apis包中函数绑定 9 | * `service`: 业务处理层 10 | * `terminal`: 提供web界面进入pod和webssh 11 | *** 12 | ## 2.如何运行(本地运行) 13 | * ①首先你需要一个k8s集群,用k8s master节点下的/root/.kube/config替换当前项目./config/.kube/default/config 14 | * ②如果你的本地与k8s集群在同一内网环境,然后就可以直接运行了 15 | * ③如果你的本地与k8s集群不在同一内网环境(比如集群在云端),本地运行会报错,需要扩展k8s访问地址,[k8s扩展地址参考](https://blog.csdn.net/marlinlm/article/details/122166105) 16 | * ④扩展地址后,将config放在对应目录`./config/.kube/config`,加载配置文件的代码在client包 17 | * ⑤将config文件的server修改为你扩展的外网ip,然后直接运行 18 | * ⑥terminal.go的使用需要websocket,可使用新版本的postman,如需使用webssh 请配置正确的账户(apis/terminal.go中) 支持password 或publickey 19 | *** 20 | ## 3.Sample 21 | * 查看k8s集群的版本:`http://127.0.0.1:10010/cluster/version/default` 22 | ```json 23 | { 24 | "major": "1", 25 | "minor": "18", 26 | "gitVersion": "v1.18.2", 27 | "gitCommit": "52c56ce7a8272c798dbc29846288d7cd9fbae032", 28 | "gitTreeState": "clean", 29 | "buildDate": "2020-04-16T11:48:36Z", 30 | "goVersion": "go1.13.9", 31 | "compiler": "gc", 32 | "platform": "linux/amd64" 33 | } 34 | ``` 35 | *** 36 | * 查看k8s集群节点详情 `http://127.0.0.1:10010/cluster/nodes/default` 37 | ```json 38 | [ 39 | { 40 | "name": "master", 41 | "status": "True", 42 | "taints": null, 43 | "os_image": "CentOS Linux 7 (Core)", 44 | "internal_ip": "10.0.8.10", 45 | "kernel_version": "3.10.0-1160.45.1.el7.x86_64", 46 | "kubelet_version": "v1.18.2", 47 | "creation_timestamp": "2022-04-05T03:28:46+08:00", 48 | "container_runtime_version": "docker://19.3.8" 49 | }, 50 | { 51 | "name": "node1", 52 | "status": "True", 53 | "taints": null, 54 | "os_image": "CentOS Linux 7 (Core)", 55 | "internal_ip": "10.0.8.12", 56 | "kernel_version": "3.10.0-1160.45.1.el7.x86_64", 57 | "kubelet_version": "v1.18.2", 58 | "creation_timestamp": "2022-04-05T03:31:54+08:00", 59 | "container_runtime_version": "docker://19.3.8" 60 | } 61 | ] 62 | ``` 63 | *** 64 | * 查看k8s集群状态 `http://127.0.0.1:10010/cluster/extra/info/default` 65 | ```json lines 66 | { 67 | "used_cpu": 2.0100000000000002, //已使用的cpu 68 | "total_cpu": 10, //总cpu 69 | "used_memory": 1648361472, //已使用的内存 70 | "total_memory": 15473422336, //总内存 71 | "readyNodeNum": 4, //就绪节点数量 72 | "totalNodeNum": 4 //总节点数量 73 | } 74 | ``` 75 | *** 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /apis/cluster.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "kubernetes-admin-backend/proto" 7 | "kubernetes-admin-backend/service" 8 | "net/http" 9 | ) 10 | 11 | // AddCluster 导入一个集群 12 | func AddCluster(c *gin.Context) { 13 | name := c.Param("clusterName") 14 | file, err := c.FormFile("file") 15 | if err != nil { 16 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, fmt.Sprintf("get form err: %s", err.Error()))) 17 | return 18 | } 19 | 20 | if err = service.AddCluster(name, file); err != nil { 21 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, fmt.Sprintf("AddCluster err: %s", err.Error()))) 22 | return 23 | } 24 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, nil, "导入集群成功")) 25 | } 26 | 27 | // GetClusters 查询所有集群 28 | func GetClusters(c *gin.Context) { 29 | clusters := service.GetClusters() 30 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, clusters, "查询集群成功")) 31 | } 32 | 33 | func DeleteCluster(c *gin.Context) { 34 | name := c.Param("clusterName") 35 | if err := service.DeleteCluster(name); err != nil { 36 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, fmt.Sprintf("删除集群失败: %s", err.Error()))) 37 | return 38 | } 39 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, nil, "删除集群成功")) 40 | } 41 | 42 | // Version 查询集群的版本信息 43 | func Version(c *gin.Context) { 44 | clusterName := c.Param("clusterName") 45 | version, _ := service.Version(clusterName) 46 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, version, "查询成功")) 47 | } 48 | 49 | // Nodes 查询节点列表详情 50 | func Nodes(c *gin.Context) { 51 | clusterName := c.Param("clusterName") 52 | nodeList, _ := service.ListClusters(clusterName) 53 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, nodeList, "查询成功")) 54 | } 55 | 56 | // ExtraClusterInfo 统计就绪节点、cpu使用、内存使用占比 57 | func ExtraClusterInfo(c *gin.Context) { 58 | clusterName := c.Param("clusterName") 59 | extraClusterInfo := service.ExtraClusterInfo(clusterName) 60 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, extraClusterInfo, "查询成功")) 61 | } 62 | 63 | // ApiGroups 查询api-resources列表 64 | func ApiGroups(c *gin.Context) { 65 | clusterName := c.Param("clusterName") 66 | groups := service.QueryApiGroups(clusterName) 67 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, groups, "查询成功")) 68 | } 69 | -------------------------------------------------------------------------------- /apis/common.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "kubernetes-admin-backend/proto" 8 | "kubernetes-admin-backend/service" 9 | "net/http" 10 | "sigs.k8s.io/yaml" 11 | ) 12 | 13 | // GetYaml 通过gvr查询资源的yaml 14 | func GetYaml(c *gin.Context) { 15 | unstructured := service.GetYaml(c.Param("clusterName"), c.Param("group"), c.Param("version"), c.Param("resource"), 16 | c.Param("namespace"), c.Param("name")) 17 | bytes, _ := yaml.Marshal(unstructured) 18 | 19 | fmt.Println(string(bytes)) 20 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, bytes, "查询yaml成功")) 21 | } 22 | 23 | // ApplyYaml 通过yaml更新资源 如果不存在 24 | func ApplyYaml(c *gin.Context) { 25 | u, err := getYamlData(c) 26 | if err != nil { 27 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, "更新失败,"+err.Error())) 28 | return 29 | } 30 | 31 | clusterName := c.Param("clusterName") 32 | unStructured := service.ApplyYaml(clusterName, u) 33 | bytes, _ := yaml.Marshal(unStructured) 34 | fmt.Println(string(bytes)) 35 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, bytes, "更新yaml成功")) 36 | } 37 | 38 | // FindGVRs 查询gvr 39 | func FindGVRs(c *gin.Context) { 40 | clusterName := c.Param("clusterName") 41 | group := c.Param("group") 42 | version := c.Param("version") 43 | kind := c.Param("kind") 44 | 45 | service.FindGVRs(clusterName, group, version, kind) 46 | } 47 | 48 | func getYamlData(c *gin.Context) (*unstructured.Unstructured, error) { 49 | var body map[string][]byte 50 | if err := c.ShouldBind(&body); err != nil { 51 | return nil, err 52 | } 53 | 54 | var u *unstructured.Unstructured 55 | if err := yaml.Unmarshal(body["data"], &u); err != nil { 56 | return nil, err 57 | } 58 | // managedFields字段不能patch 也不建议update 如果存在则删除 59 | delete(u.Object["metadata"].(map[string]interface{}), "managedFields") 60 | return u, nil 61 | } 62 | -------------------------------------------------------------------------------- /apis/configmap.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "kubernetes-admin-backend/proto" 7 | "kubernetes-admin-backend/service" 8 | "net/http" 9 | "sigs.k8s.io/yaml" 10 | ) 11 | 12 | // ListConfigMap 根据命名空间查询configmap,命名空间为空字符串时查询所有 13 | func ListConfigMap(c *gin.Context) { 14 | clusterName := c.Param("clusterName") 15 | namespace := c.DefaultQuery("namespace", "") 16 | configMaps := service.ListConfigMap(clusterName, namespace) 17 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, configMaps, "查询成功")) 18 | } 19 | 20 | // GetConfigMapByName 根据名称查询configmap的yaml信息 21 | func GetConfigMapByName(c *gin.Context) { 22 | clusterName := c.Param("clusterName") 23 | namespace := c.Param("namespace") 24 | name := c.Param("name") 25 | unstructured := service.GetConfigMapByName(clusterName, namespace, name) 26 | //bytes, _ := yaml.Marshal(unstructObj) 27 | bytes, _ := yaml.Marshal(unstructured) 28 | fmt.Println(string(bytes)) 29 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, bytes, "查询成功")) 30 | } 31 | -------------------------------------------------------------------------------- /apis/event.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "kubernetes-admin-backend/proto" 6 | "kubernetes-admin-backend/service" 7 | "net/http" 8 | ) 9 | 10 | func ListEvent(c *gin.Context) { 11 | clusterName := c.Param("clusterName") 12 | namespace := c.DefaultQuery("namespace", "") 13 | eventList := service.ListEvent(clusterName, namespace) 14 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, eventList, "查询成功")) 15 | } 16 | -------------------------------------------------------------------------------- /apis/namespace.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "kubernetes-admin-backend/proto" 6 | "kubernetes-admin-backend/service" 7 | "net/http" 8 | ) 9 | 10 | // GetNamespaces 查询所有命名空间 11 | func GetNamespaces(c *gin.Context) { 12 | clusterName := c.Param("clusterName") 13 | namespaces, err := service.GetNamespaces(clusterName) 14 | if err != nil { 15 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, err.Error())) 16 | return 17 | } 18 | 19 | nsList := make([]proto.NameSpace, 0, len(namespaces)) 20 | for _, namespace := range namespaces { 21 | var ns proto.NameSpace 22 | ns.Name = namespace.Name 23 | ns.Labels = namespace.Labels 24 | ns.Annotations = namespace.Annotations 25 | ns.CreationTimestamp = namespace.CreationTimestamp.Time 26 | ns.Status = string(namespace.Status.Phase) 27 | nsList = append(nsList, ns) 28 | } 29 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, nsList, "查询成功")) 30 | } 31 | 32 | // CreateNamespace 创建命名空间 33 | func CreateNamespace(c *gin.Context) { 34 | clusterName := c.Param("clusterName") 35 | var nameSpace proto.NameSpace 36 | if err := c.ShouldBind(&nameSpace); err != nil { 37 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, err.Error())) 38 | return 39 | } 40 | 41 | namespace, err := service.CreateNamespace(clusterName, nameSpace) 42 | if err != nil { 43 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, err.Error())) 44 | return 45 | } 46 | 47 | ns := proto.NameSpace{Name: namespace.Name, Labels: namespace.Labels, Annotations: namespace.Annotations, 48 | CreationTimestamp: namespace.CreationTimestamp.Time, Status: string(namespace.Status.Phase)} 49 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, ns, "创建成功")) 50 | } 51 | 52 | // DeleteNamespace 根据名称删除命名空间 53 | func DeleteNamespace(c *gin.Context) { 54 | clusterName := c.Param("clusterName") 55 | if err := service.DeleteNamespace(clusterName, c.Param("name")); err != nil { 56 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, err.Error())) 57 | return 58 | } 59 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, nil, "删除成功")) 60 | } 61 | 62 | // UpdateNamespace 修改命名空间 63 | func UpdateNamespace(c *gin.Context) { 64 | clusterName := c.Param("clusterName") 65 | var nameSpace proto.NameSpace 66 | if err := c.ShouldBind(&nameSpace); err != nil { 67 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, err.Error())) 68 | return 69 | } 70 | 71 | namespace, err := service.UpdateNamespace(clusterName, nameSpace) 72 | if err != nil { 73 | c.JSON(http.StatusInternalServerError, (&proto.Result{}).Error(500, nil, err.Error())) 74 | return 75 | } 76 | 77 | nameSpace.Status = string(namespace.Status.Phase) 78 | nameSpace.CreationTimestamp = namespace.CreationTimestamp.Time 79 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, nameSpace, "修改成功")) 80 | } 81 | -------------------------------------------------------------------------------- /apis/pod.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "kubernetes-admin-backend/proto" 6 | "kubernetes-admin-backend/service" 7 | "net/http" 8 | ) 9 | 10 | // GetAllPods 查询所有pods 11 | func GetAllPods(c *gin.Context) { 12 | clusterName := c.Param("clusterName") 13 | pods := service.GetPods(clusterName) 14 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, pods, "查询成功")) 15 | } 16 | 17 | // todo 查询某一命名空间下的pod 18 | -------------------------------------------------------------------------------- /apis/svc.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "kubernetes-admin-backend/proto" 6 | "kubernetes-admin-backend/service" 7 | "net/http" 8 | ) 9 | 10 | // ListSvc 根据命名空间和标签查询svc,为空字符串时该条件为所有 11 | func ListSvc(c *gin.Context) { 12 | clusterName := c.Param("clusterName") 13 | namespace := c.DefaultQuery("namespace", "") 14 | label := c.DefaultQuery("label", "") 15 | svcs := service.ListSvc(clusterName, namespace, label) 16 | c.JSON(http.StatusOK, (&proto.Result{}).Ok(200, svcs, "查询成功")) 17 | } 18 | -------------------------------------------------------------------------------- /apis/terminal.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/gorilla/websocket" 8 | "kubernetes-admin-backend/terminal" 9 | "net/http" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | //包级变量,升级器 15 | var upgrader = websocket.Upgrader{} 16 | 17 | func init() { 18 | //初始化 19 | upgrader = websocket.Upgrader{ 20 | ReadBufferSize: 1024, 21 | WriteBufferSize: 1024, 22 | // 解决跨域问题 23 | CheckOrigin: func(r *http.Request) bool { 24 | return true 25 | }, 26 | } 27 | } 28 | 29 | // TerminalPod web中进入pod中的容器终端 30 | func TerminalPod(c *gin.Context) { 31 | wsConn, err := terminal.InitWebsocket(c.Writer, c.Request) 32 | if err != nil { 33 | fmt.Println("InitWebsocket err", err) 34 | wsConn.WsClose() 35 | return 36 | } 37 | clusterName := c.Param("clusterName") 38 | namespace := c.Param("namespace") 39 | podName := c.Param("podName") 40 | container := c.Param("container") 41 | 42 | wsConn.WsWrite(websocket.TextMessage, []byte("你已进入 命名空间:"+namespace+" 容器组:"+podName+" 容器名:"+container+"的终端")) 43 | 44 | if err := terminal.StartProcess(wsConn, clusterName, podName, namespace, container); err != nil { 45 | fmt.Println("StartProcess err", err) 46 | wsConn.WsClose() 47 | return 48 | } 49 | } 50 | 51 | // VisitorWebsocketServer https://github.com/widaT/webssh websocket连接实现webssh 52 | func VisitorWebsocketServer(c *gin.Context) { 53 | wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 54 | if err != nil { 55 | fmt.Println("upgrade error:", err) 56 | return 57 | } 58 | defer wsConn.Close() 59 | 60 | config := &terminal.SSHClientConfig{ 61 | Timeout: time.Second * 5, 62 | HostAddr: "xxx.xxx.xxx.xxx:22", 63 | User: "*****", 64 | Password: "*****", 65 | AuthModel: "PASSWORD", 66 | } 67 | sshClient, err := terminal.NewSSHClient(config) 68 | if err != nil { 69 | wsConn.WriteControl(websocket.CloseMessage, 70 | []byte(err.Error()), time.Now().Add(time.Second)) 71 | return 72 | } 73 | defer sshClient.Close() 74 | 75 | turn, err := terminal.NewTurn(wsConn, sshClient) 76 | if err != nil { 77 | fmt.Println("NewTurn," + err.Error()) 78 | wsConn.WriteControl(websocket.CloseMessage, 79 | []byte(err.Error()), time.Now().Add(time.Second)) 80 | return 81 | } 82 | defer turn.Close() 83 | 84 | ctx, cancel := context.WithCancel(context.Background()) 85 | wg := sync.WaitGroup{} 86 | wg.Add(2) 87 | go func() { 88 | defer wg.Done() 89 | err := turn.LoopRead(ctx) 90 | if err != nil { 91 | fmt.Printf("%#v", err) 92 | } 93 | }() 94 | go func() { 95 | defer wg.Done() 96 | err := turn.SessionWait() 97 | if err != nil { 98 | fmt.Printf("%#v", err) 99 | } 100 | cancel() 101 | }() 102 | wg.Wait() 103 | } 104 | -------------------------------------------------------------------------------- /config/.kube/default/config: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | clusters: 3 | - cluster: 4 | certificate-authority-data: xxxxxxxxxxxxxxxxxxxxxx 5 | server: https://xxx.xxx.xxx.xxx:6443 6 | name: kubernetes 7 | contexts: 8 | - context: 9 | cluster: kubernetes 10 | user: kubernetes-admin 11 | name: kubernetes-admin@kubernetes 12 | current-context: kubernetes-admin@kubernetes 13 | kind: Config 14 | preferences: {} 15 | users: 16 | - name: kubernetes-admin 17 | user: 18 | client-certificate-data: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 19 | client-key-data: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 20 | -------------------------------------------------------------------------------- /config/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | name: client-go 3 | host: 0.0.0.0 4 | port: 10010 -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "k8s.io/apimachinery/pkg/util/yaml" 7 | "k8s.io/klog" 8 | "os" 9 | ) 10 | 11 | type KeyName string 12 | 13 | const ( 14 | ServerName KeyName = "server_name" 15 | ServerHost KeyName = "server_host" 16 | ServerPort KeyName = "server_port" 17 | ) 18 | 19 | var keyMap map[KeyName]interface{} 20 | 21 | type Config struct { 22 | Server Server 23 | } 24 | 25 | type Server struct { 26 | Name string `yaml:"name"` 27 | Host string `yaml:"host"` 28 | Port int `yaml:"port"` 29 | } 30 | 31 | func init() { 32 | var config Config 33 | yamlFile, err := ioutil.ReadFile("./config/application.yaml") 34 | if err != nil { 35 | klog.Fatal(err) 36 | return 37 | } 38 | err = yaml.Unmarshal(yamlFile, &config) 39 | if err != nil { 40 | klog.Fatal(err) 41 | return 42 | } 43 | fmt.Println(os.Getwd()) 44 | keyMap = make(map[KeyName]interface{}) 45 | keyMap[ServerName] = config.Server.Name 46 | keyMap[ServerHost] = config.Server.Host 47 | keyMap[ServerPort] = config.Server.Port 48 | } 49 | 50 | func GetString(keyName KeyName) string { 51 | return keyMap[keyName].(string) 52 | } 53 | 54 | func GetInt(keyName KeyName) int { 55 | return keyMap[keyName].(int) 56 | } 57 | -------------------------------------------------------------------------------- /docker-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | tag=$1 3 | ############################################### docker部署未验证 ##################################################### 4 | #go env -w GOPROXY=https://goproxy.cn,direct 5 | #go mod tidy 6 | 7 | SET CGO_ENABLED=0 SET GOOS=linux SET GOARCH=amd64 go build -o ./bin/linux_amd64/main main.go 8 | docker build -t kubernetes-admin-backend:$tag . 9 | docker rm -f kubernetes-admin-backend 10 | docker run -d --restart always --net host --name=kubernetes-admin-backend -p 10010:10010 kubernetes-admin-backend:$tag 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module kubernetes-admin-backend 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.7.7 7 | github.com/gorilla/websocket v1.5.0 8 | github.com/imdario/mergo v0.3.7 // indirect 9 | github.com/prometheus/client_golang v1.12.1 10 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 11 | k8s.io/api v0.20.1 12 | k8s.io/apimachinery v0.20.1 13 | k8s.io/client-go v0.20.1 14 | k8s.io/klog v1.0.0 15 | sigs.k8s.io/yaml v1.2.0 16 | ) 17 | -------------------------------------------------------------------------------- /k8s-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################################### k8s部署未验证 ##################################################### 4 | kubectl apply -f k8s-kubernetes-admin-backend.yaml 5 | -------------------------------------------------------------------------------- /k8s-kubernetes-admin-backend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kubeadmin-backend 5 | namespace: default 6 | labels: 7 | name: kubeadmin-deploy 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | name: kubeadmin-pod 13 | template: 14 | metadata: 15 | labels: 16 | name: kubeadmin-pod 17 | spec: 18 | containers: 19 | - name: "kubeadmin-backend" 20 | image: kubernetes-admin-backend:v1.0.0 #注意切换正确的镜像 21 | imagePullPolicy: IfNotPresent 22 | ports: 23 | - containerPort: 10010 24 | --- 25 | apiVersion: v1 26 | kind: Service 27 | metadata: 28 | name: kubeadmin-service 29 | namespace: default 30 | labels: 31 | name: kubeadmin-service 32 | spec: 33 | selector: 34 | name: kubeadmin-pod 35 | type: NodePort 36 | ports: 37 | - name: "kube-admin" 38 | port: 10010 39 | targetPort: 10010 40 | nodePort: 30010 41 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "k8s.io/klog" 7 | "kubernetes-admin-backend/config" 8 | "kubernetes-admin-backend/middleware" 9 | _ "kubernetes-admin-backend/proto" 10 | "kubernetes-admin-backend/router" 11 | ) 12 | 13 | func main() { 14 | 15 | //go service.Informers() //todo informers监控 16 | engine := gin.Default() //返回默认的路由引擎 17 | gin.SetMode(gin.DebugMode) 18 | engine.Use(middleware.Cors(), middleware.Monitor()) 19 | router.CollectRoute(engine) 20 | err := engine.Run(fmt.Sprintf("%s:%d", config.GetString(config.ServerHost), config.GetInt(config.ServerPort))) 21 | if err != nil { 22 | klog.Fatal(err) 23 | return 24 | } 25 | } 26 | 27 | //engine.GET("/metrics", promHandler(promhttp.Handler())) 28 | /*func promHandler(handler http.Handler) gin.HandlerFunc { 29 | return func(c *gin.Context) { 30 | handler.ServeHTTP(c.Writer, c.Request) 31 | } 32 | }*/ 33 | -------------------------------------------------------------------------------- /middleware/Cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | // Cors 跨域问题 9 | func Cors() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | method := c.Request.Method 12 | c.Header("Access-Control-Allow-Origin", "*") 13 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,PUT,PATCH,DELETE") 14 | c.Header("Access-Control-Allow-Headers", "content-type,origin, authorization, accept") 15 | c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 16 | c.Header("Content-Type", "application/json") 17 | if method == "OPTIONS" { 18 | c.AbortWithStatus(http.StatusNoContent) 19 | } 20 | c.Next() //处理请求 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /middleware/Monitor.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/push" 8 | "time" 9 | ) 10 | 11 | var WebRequestTotal = prometheus.NewCounterVec( 12 | prometheus.CounterOpts{ 13 | Name: "web_request_total", 14 | Help: "Number of requests in total", 15 | }, 16 | // 设置两个标签 请求方法和 路径 对请求总次数在两个 17 | []string{"method", "endpoint"}, 18 | ) 19 | 20 | var WebRequestDuration = prometheus.NewHistogramVec( 21 | prometheus.HistogramOpts{ 22 | Name: "web_request_duration_seconds", 23 | Help: "web request duration distribution", 24 | Buckets: []float64{0.1, 0.3, 0.5, 0.7, 0.9, 1.5}, 25 | }, 26 | []string{"method", "endpoint"}, 27 | ) 28 | 29 | var WebActiveTotal = prometheus.NewGauge( 30 | prometheus.GaugeOpts{ 31 | Name: "web_active_total", 32 | Help: "web active total", 33 | }) 34 | 35 | // Monitor 监控 36 | func Monitor() gin.HandlerFunc { 37 | // 注册监控指标 38 | prometheus.MustRegister(WebActiveTotal) 39 | prometheus.MustRegister(WebRequestTotal) 40 | prometheus.MustRegister(WebRequestDuration) 41 | 42 | return func(c *gin.Context) { 43 | start := time.Now() 44 | c.Next() 45 | duration := time.Since(start) 46 | //WebActiveTotal.Set(float64(r.Session.Size())) 47 | WebRequestTotal.With(prometheus.Labels{"method": c.Request.Method, "endpoint": c.Request.URL.Path}).Inc() 48 | WebRequestDuration.With(prometheus.Labels{"method": c.Request.Method, "endpoint": c.Request.URL.Path}).Observe(duration.Seconds()) 49 | } 50 | 51 | } 52 | 53 | func ExamplePusher_Push() { 54 | completionTime := prometheus.NewGauge(prometheus.GaugeOpts{ 55 | Name: "db_backup_last_completion_timestamp_seconds", 56 | Help: "The timestamp of the last successful completion of a DB backup.", 57 | }) 58 | completionTime.SetToCurrentTime() 59 | if err := push.New("http://pushgateway:9091", "db_backup"). 60 | Collector(completionTime). 61 | Grouping("db", "customers"). 62 | Push(); err != nil { 63 | fmt.Println("Could not push completion time to Pushgateway:", err) 64 | } 65 | } 66 | 67 | var ( 68 | completionTime = prometheus.NewGauge(prometheus.GaugeOpts{ 69 | Name: "db_backup_last_completion_timestamp_seconds", 70 | Help: "The timestamp of the last completion of a DB backup, successful or not.", 71 | }) 72 | successTime = prometheus.NewGauge(prometheus.GaugeOpts{ 73 | Name: "db_backup_last_success_timestamp_seconds", 74 | Help: "The timestamp of the last successful completion of a DB backup.", 75 | }) 76 | duration = prometheus.NewGauge(prometheus.GaugeOpts{ 77 | Name: "db_backup_duration_seconds", 78 | Help: "The duration of the last DB backup in seconds.", 79 | }) 80 | records = prometheus.NewGauge(prometheus.GaugeOpts{ 81 | Name: "db_backup_records_processed", 82 | Help: "The number of records processed in the last DB backup.", 83 | }) 84 | ) 85 | 86 | func performBackup() (int, error) { 87 | // Perform the backup and return the number of backed up records and any 88 | // applicable error. 89 | // ... 90 | return 42, nil 91 | } 92 | 93 | func ExamplePusher_Add() { 94 | // We use a registry here to benefit from the consistency checks that 95 | // happen during registration. 96 | registry := prometheus.NewRegistry() 97 | registry.MustRegister(completionTime, duration, records) 98 | // Note that successTime is not registered. 99 | 100 | pusher := push.New("http://pushgateway:9091", "db_backup").Gatherer(registry) 101 | 102 | start := time.Now() 103 | n, err := performBackup() 104 | records.Set(float64(n)) 105 | // Note that time.Since only uses a monotonic clock in Go1.9+. 106 | duration.Set(time.Since(start).Seconds()) 107 | completionTime.SetToCurrentTime() 108 | if err != nil { 109 | fmt.Println("DB backup failed:", err) 110 | } else { 111 | // Add successTime to pusher only in case of success. 112 | // We could as well register it with the registry. 113 | // This example, however, demonstrates that you can 114 | // mix Gatherers and Collectors when handling a Pusher. 115 | pusher.Collector(successTime) 116 | successTime.SetToCurrentTime() 117 | } 118 | // Add is used here rather than Push to not delete a previously pushed 119 | // success timestamp in case of a failure of this backup. 120 | if err := pusher.Add(); err != nil { 121 | fmt.Println("Could not push to Pushgateway:", err) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /proto/Cluster.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "k8s.io/client-go/dynamic" 10 | "k8s.io/client-go/kubernetes" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/tools/clientcmd" 13 | "k8s.io/klog" 14 | "mime/multipart" 15 | "os" 16 | "sync" 17 | ) 18 | 19 | type Cluster struct { 20 | Name string `json:"name"` 21 | Config *rest.Config `json:"-"` 22 | ClientSet *kubernetes.Clientset `json:"-"` 23 | DynamicClient dynamic.Interface `json:"-"` 24 | } 25 | 26 | // ClusterStore 集群配置存储接口,不引入其他存储应用,默认实现存入项目路径中 27 | type ClusterStore interface { 28 | AddByFile(name string, file *multipart.FileHeader) error 29 | //AddByCertificate(name string,file *multipart.FileHeader) error 30 | //AddByToken(name string,file *multipart.FileHeader) error 31 | Delete(name string) error 32 | Get(name string) (Cluster, error) 33 | List() []Cluster 34 | } 35 | 36 | var clusters sync.Map 37 | 38 | type DefaultClusterStore struct { 39 | } 40 | 41 | const fixedConfigPath = "./config/.kube" 42 | 43 | func init() { 44 | klog.Info("加载多集群配置信息...") 45 | 46 | fileInfoList, _ := ioutil.ReadDir(fixedConfigPath) 47 | for _, info := range fileInfoList { 48 | name := info.Name() 49 | KubeConfig, _ := clientcmd.BuildConfigFromFlags("", fixedConfigPath+"/"+name+"/"+"config") 50 | cache(name, KubeConfig) 51 | } 52 | } 53 | 54 | func (c DefaultClusterStore) AddByFile(name string, fileHeader *multipart.FileHeader) (err error) { 55 | file, err := fileHeader.Open() 56 | if err != nil { 57 | klog.Fatal(err) 58 | return err 59 | } 60 | defer file.Close() 61 | //判断上传的配置是否能连接集群 62 | kubeConfig, err := isAvailable(file) 63 | if err != nil { 64 | klog.Fatal(err) 65 | return err 66 | } 67 | //缓存连接 68 | err = cache(name, kubeConfig) 69 | if err != nil { 70 | klog.Fatal(err) 71 | return err 72 | } 73 | path := fixedConfigPath + "/" + name + "/" + fileHeader.Filename 74 | os.Mkdir(fixedConfigPath+"/"+name, os.ModePerm) 75 | //保存上传的配置文件,用于程序启动时加载 76 | file.Seek(0, 0) 77 | e := saveFile(file, path) 78 | fmt.Println(e) 79 | return nil 80 | } 81 | 82 | func (c DefaultClusterStore) Delete(name string) error { 83 | dirPath := fixedConfigPath + "/" + name + "/" 84 | err := os.RemoveAll(dirPath) 85 | if err != nil { 86 | return err 87 | } 88 | clusters.Delete(name) 89 | return nil 90 | } 91 | 92 | func (c DefaultClusterStore) Get(name string) (Cluster, error) { 93 | if cluster, ok := clusters.Load(name); ok { 94 | return cluster.(Cluster), nil 95 | } else { 96 | return Cluster{}, errors.New("未查询到") 97 | } 98 | } 99 | 100 | func (c DefaultClusterStore) List() []Cluster { 101 | clusterList := make([]Cluster, 0, 0) 102 | clusters.Range(func(key, value interface{}) bool { 103 | clusterList = append(clusterList, value.(Cluster)) 104 | return true 105 | }) 106 | return clusterList 107 | } 108 | 109 | func cache(name string, kubeConfig *rest.Config) (err error) { 110 | var kubeClientSet *kubernetes.Clientset 111 | var kubeDynamicClient dynamic.Interface 112 | 113 | if kubeClientSet, err = kubernetes.NewForConfig(kubeConfig); err != nil { 114 | klog.Fatal(err) 115 | return err 116 | } 117 | if kubeDynamicClient, err = dynamic.NewForConfig(kubeConfig); err != nil { 118 | klog.Fatal(err) 119 | return err 120 | } 121 | cluster := Cluster{Name: name, Config: kubeConfig, ClientSet: kubeClientSet, DynamicClient: kubeDynamicClient} 122 | clusters.Store(name, cluster) 123 | return nil 124 | } 125 | 126 | func isAvailable(file multipart.File) (*rest.Config, error) { 127 | buf := bytes.NewBuffer(nil) 128 | if _, err := io.Copy(buf, file); err != nil { 129 | fmt.Println("io.Copy," + err.Error()) 130 | return nil, err 131 | } 132 | config, err := clientcmd.NewClientConfigFromBytes(buf.Bytes()) 133 | if err != nil { 134 | klog.Fatal("NewClientConfigFromBytes," + err.Error()) 135 | return nil, err 136 | } 137 | clientConfig, err := config.ClientConfig() 138 | if err != nil { 139 | klog.Fatal("ClientConfig," + err.Error()) 140 | return nil, err 141 | } 142 | clientSet, err := kubernetes.NewForConfig(clientConfig) 143 | if err != nil { 144 | klog.Fatal("NewForConfigorConfigFile," + err.Error()) 145 | return nil, err 146 | } 147 | _, err = clientSet.ServerVersion() 148 | if err != nil { 149 | klog.Fatal("ServerVersion," + err.Error()) 150 | return nil, err 151 | } 152 | return clientConfig, nil 153 | } 154 | 155 | func saveFile(file multipart.File, dst string) error { 156 | out, err := os.Create(dst) 157 | if err != nil { 158 | return err 159 | } 160 | defer out.Close() 161 | 162 | _, err = io.Copy(out, file) 163 | return err 164 | } 165 | -------------------------------------------------------------------------------- /proto/apiresources.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type ApiResource struct { 4 | Name string `json:"name"` 5 | Kind string `json:"kind"` 6 | ShortNames []string `json:"short_names"` 7 | Namespaced bool `json:"namespaced"` 8 | GroupVersion string `json:"group_version"` 9 | } 10 | -------------------------------------------------------------------------------- /proto/configmap.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type ConfigMap struct { 4 | Age string `json:"age"` 5 | Name string `json:"name"` 6 | Data map[string]string `json:"data"` 7 | Labels Labels `json:"labels"` 8 | Namespace string `json:"namespace"` 9 | Annotations Annotations `json:"annotations"` 10 | } 11 | -------------------------------------------------------------------------------- /proto/container.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type Container struct { 4 | Name string `json:"name"` 5 | Ready bool `json:"ready"` 6 | Image string `json:"image"` 7 | ImageId string `json:"image_id"` 8 | ContainerId string `json:"container_id"` 9 | RestartCount int `json:"restart_count"` 10 | } 11 | -------------------------------------------------------------------------------- /proto/extraclusterinfo.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type ExtraClusterInfo struct { 4 | UsedCpu float64 `json:"used_cpu"` 5 | TotalCpu float64 `json:"total_cpu"` 6 | UsedMemory float64 `json:"used_memory"` 7 | TotalMemory float64 `json:"total_memory"` 8 | ReadyNodeNum int `json:"readyNodeNum"` 9 | TotalNodeNum int `json:"totalNodeNum"` 10 | } 11 | -------------------------------------------------------------------------------- /proto/namespace.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import "time" 4 | 5 | type Labels map[string]string 6 | type Annotations map[string]string 7 | 8 | type NameSpace struct { 9 | Name string `json:"name"` 10 | Status string `json:"status"` 11 | Labels Labels `json:"labels"` 12 | Annotations Annotations `json:"annotations"` 13 | CreationTimestamp time.Time `json:"creation_timestamp"` 14 | } 15 | -------------------------------------------------------------------------------- /proto/node.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | v1 "k8s.io/api/core/v1" 5 | "time" 6 | ) 7 | 8 | type Node struct { 9 | Name string `json:"name"` 10 | Status string `json:"status"` 11 | Taints []v1.Taint `json:"taints"` 12 | Labels Labels `json:"labels"` 13 | OsImage string `json:"os_image"` 14 | InternalIp string `json:"internal_ip"` 15 | Annotations Annotations `json:"annotations"` 16 | KernelVersion string `json:"kernel_version"` 17 | KubeletVersion string `json:"kubelet_version"` 18 | CreationTimestamp time.Time `json:"creation_timestamp"` 19 | ContainerRuntimeVersion string `json:"container_runtime_version"` 20 | } 21 | -------------------------------------------------------------------------------- /proto/pod.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import "time" 4 | 5 | type Pod struct { 6 | Name string `json:"name"` 7 | PodIp string `json:"pod_ip"` 8 | Status string `json:"status"` 9 | Labels Labels `json:"labels"` 10 | NodeName string `json:"node_name"` 11 | Namespace string `json:"namespace"` 12 | Containers []Container `json:"containers"` 13 | Annotations Annotations `json:"annotations"` 14 | CreationTimestamp time.Time `json:"creation_timestamp"` 15 | } 16 | -------------------------------------------------------------------------------- /proto/result.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type Result struct { 4 | Code int `json:"code"` 5 | Success bool `json:"success"` 6 | Data interface{} `json:"data"` 7 | Msg string `json:"msg"` 8 | } 9 | 10 | func (result *Result) Ok(code int, data interface{}, msg string) *Result { 11 | result.Code = code 12 | result.Success = true 13 | result.Data = data 14 | result.Msg = msg 15 | return result 16 | } 17 | 18 | func (result *Result) Error(code int, data interface{}, msg string) *Result { 19 | result.Code = code 20 | result.Success = false 21 | result.Data = data 22 | result.Msg = msg 23 | return result 24 | } 25 | -------------------------------------------------------------------------------- /proto/svc.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | type Selector map[string]string 4 | type Svc struct { 5 | Name string `json:"name"` 6 | Namespace string `json:"namespace"` 7 | Type string `json:"type"` 8 | ClusterIp string `json:"cluster_ip"` 9 | Ports []string `json:"ports"` 10 | Selector Selector `json:"selector"` 11 | Labels Labels `json:"labels"` 12 | Annotations Annotations `json:"annotations"` 13 | EndPoints []string `json:"end_points"` 14 | } 15 | -------------------------------------------------------------------------------- /router/routes.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/prometheus/client_golang/prometheus/promhttp" 6 | "kubernetes-admin-backend/apis" 7 | "net/http" 8 | ) 9 | 10 | func CollectRoute(engine *gin.Engine) { 11 | // prometheus 监控 12 | engine.GET("/metrics", func(handler http.Handler) gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | handler.ServeHTTP(c.Writer, c.Request) 15 | } 16 | }(promhttp.Handler())) 17 | 18 | engine.GET("/terminal/ssh", apis.VisitorWebsocketServer) 19 | engine.GET("/terminal/pod/:clusterName/:namespace/:podName/:container", apis.TerminalPod) 20 | 21 | commonGroup := engine.Group("/common") 22 | commonGroup.GET("/:clusterName/:group/:version/:resource/:namespace/:name", apis.GetYaml) 23 | commonGroup.POST("/apply/:clusterName", apis.ApplyYaml) 24 | commonGroup.GET("/find/gvr/:clusterName/:group/:version/:kind", apis.FindGVRs) 25 | 26 | clusterGroup := engine.Group("/cluster") 27 | clusterGroup.GET("/version/:clusterName", apis.Version) 28 | clusterGroup.GET("/nodes/:clusterName", apis.Nodes) 29 | clusterGroup.GET("/extra/info/:clusterName", apis.ExtraClusterInfo) 30 | clusterGroup.GET("/groups/:clusterName", apis.ApiGroups) 31 | clusterGroup.POST("/add/:clusterName", apis.AddCluster) 32 | clusterGroup.GET("/list", apis.GetClusters) 33 | clusterGroup.GET("/delete/:clusterName", apis.DeleteCluster) 34 | 35 | namespaceGroup := engine.Group("/namespace") 36 | namespaceGroup.GET("/get/:clusterName", apis.GetNamespaces) 37 | namespaceGroup.POST("/create/:clusterName", apis.CreateNamespace) 38 | namespaceGroup.POST("/delete/:clusterName/:name", apis.DeleteNamespace) 39 | namespaceGroup.POST("/update/:clusterName", apis.UpdateNamespace) 40 | 41 | podGroup := engine.Group("/pod") 42 | podGroup.GET("/list/:clusterName", apis.GetAllPods) 43 | 44 | svcGroup := engine.Group("/svc") 45 | svcGroup.GET("/list/:clusterName", apis.ListSvc) 46 | 47 | eventGroup := engine.Group("/event") 48 | eventGroup.GET("/list/:clusterName", apis.ListEvent) 49 | 50 | configMapsGroup := engine.Group("/configmaps") 51 | configMapsGroup.GET("/list/:clusterName", apis.ListConfigMap) 52 | configMapsGroup.GET("/get/:clusterName/:namespace/:name", apis.GetConfigMapByName) 53 | } 54 | -------------------------------------------------------------------------------- /service/cluster.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/version" 8 | "k8s.io/klog" 9 | "kubernetes-admin-backend/proto" 10 | "mime/multipart" 11 | ) 12 | 13 | func AddCluster(name string, fileHeader *multipart.FileHeader) error { 14 | defaultClusterStore := proto.DefaultClusterStore{} 15 | return addCluster(defaultClusterStore, name, fileHeader) 16 | } 17 | 18 | func addCluster(store proto.ClusterStore, name string, fileHeader *multipart.FileHeader) error { 19 | return store.AddByFile(name, fileHeader) 20 | } 21 | 22 | func GetClusters() []proto.Cluster { 23 | defaultClusterStore := proto.DefaultClusterStore{} 24 | return getClusters(defaultClusterStore) 25 | } 26 | 27 | func getClusters(store proto.DefaultClusterStore) []proto.Cluster { 28 | return store.List() 29 | } 30 | 31 | func GetCluster(name string) (proto.Cluster, error) { 32 | defaultClusterStore := proto.DefaultClusterStore{} 33 | return defaultClusterStore.Get(name) 34 | } 35 | 36 | func DeleteCluster(name string) error { 37 | defaultClusterStore := proto.DefaultClusterStore{} 38 | return deleteCluster(defaultClusterStore, name) 39 | } 40 | 41 | func deleteCluster(store proto.ClusterStore, name string) error { 42 | return store.Delete(name) 43 | } 44 | 45 | func Version(clusterName string) (*version.Info, error) { 46 | cluster, _ := GetCluster(clusterName) 47 | clientSet := cluster.ClientSet 48 | 49 | version, err := clientSet.ServerVersion() 50 | if err != nil { 51 | klog.Error(err) 52 | return nil, err 53 | } 54 | return version, nil 55 | } 56 | 57 | func ListClusters(clusterName string) ([]proto.Node, error) { 58 | cluster, _ := GetCluster(clusterName) 59 | clientSet := cluster.ClientSet 60 | 61 | ctx := context.Background() 62 | nodeList, err := clientSet.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 63 | if err != nil { 64 | klog.Error(err) 65 | return nil, err 66 | } 67 | 68 | nodes := make([]proto.Node, 0, 5) 69 | for _, item := range nodeList.Items { 70 | node := proto.Node{Name: item.Name, Labels: item.Labels, Annotations: item.Annotations, CreationTimestamp: item.CreationTimestamp.Time, 71 | Taints: item.Spec.Taints, Status: getReadyStatus(item.Status.Conditions), InternalIp: getInternalIp(item.Status.Addresses), 72 | KernelVersion: item.Status.NodeInfo.KernelVersion, KubeletVersion: item.Status.NodeInfo.KubeletVersion, 73 | ContainerRuntimeVersion: item.Status.NodeInfo.ContainerRuntimeVersion, OsImage: item.Status.NodeInfo.OSImage} 74 | nodes = append(nodes, node) 75 | } 76 | return nodes, nil 77 | } 78 | 79 | func getInternalIp(addresses []v1.NodeAddress) string { 80 | for _, address := range addresses { 81 | if address.Type == v1.NodeInternalIP { 82 | return address.Address 83 | } 84 | } 85 | return "notfound" 86 | } 87 | 88 | func getReadyStatus(conditions []v1.NodeCondition) string { 89 | for _, condition := range conditions { 90 | if condition.Type == v1.NodeReady { 91 | return string(condition.Status) 92 | } 93 | } 94 | return "notfound" 95 | } 96 | 97 | func ExtraClusterInfo(clusterName string) proto.ExtraClusterInfo { 98 | extraClusterInfo := proto.ExtraClusterInfo{0, 0, 0, 0, 0, 0} 99 | 100 | ctx := context.Background() 101 | cluster, _ := GetCluster(clusterName) 102 | clientSet := cluster.ClientSet 103 | 104 | nodeList, _ := clientSet.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 105 | nodes := nodeList.Items 106 | extraClusterInfo.TotalNodeNum = len(nodes) 107 | for i := range nodes { 108 | conditions := nodes[i].Status.Conditions 109 | for i := range conditions { 110 | if conditions[i].Type == "Ready" { 111 | if conditions[i].Status == "True" { 112 | extraClusterInfo.ReadyNodeNum += 1 113 | } 114 | } 115 | } 116 | cpu := nodes[i].Status.Allocatable.Cpu().AsApproximateFloat64() 117 | extraClusterInfo.TotalCpu += cpu 118 | memory := nodes[i].Status.Allocatable.Memory().AsApproximateFloat64() 119 | extraClusterInfo.TotalMemory += memory 120 | } 121 | 122 | podsList, _ := clientSet.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) 123 | pods := podsList.Items 124 | for i := range pods { 125 | for j := range pods[i].Spec.Containers { 126 | cpu := pods[i].Spec.Containers[j].Resources.Requests.Cpu().AsApproximateFloat64() 127 | extraClusterInfo.UsedCpu += cpu 128 | memory := pods[i].Spec.Containers[j].Resources.Requests.Memory().AsApproximateFloat64() 129 | extraClusterInfo.UsedMemory += memory 130 | } 131 | } 132 | return extraClusterInfo 133 | } 134 | 135 | func QueryApiGroups(clusterName string) []proto.ApiResource { 136 | apiResources := make([]proto.ApiResource, 0, 20) 137 | cluster, _ := GetCluster(clusterName) 138 | clientSet := cluster.ClientSet 139 | _, resources, _ := clientSet.ServerGroupsAndResources() 140 | for _, resource := range resources { 141 | for _, api := range resource.APIResources { 142 | apiResource := proto.ApiResource{Name: api.Name, ShortNames: api.ShortNames, Kind: api.Kind, Namespaced: api.Namespaced, GroupVersion: resource.GroupVersion} 143 | apiResources = append(apiResources, apiResource) 144 | } 145 | } 146 | return apiResources 147 | } 148 | -------------------------------------------------------------------------------- /service/common.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | //"k8s.io/apimachinery/pkg/api/meta" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/apimachinery/pkg/types" 11 | "k8s.io/client-go/discovery" 12 | "k8s.io/client-go/discovery/cached/memory" 13 | "k8s.io/client-go/restmapper" 14 | "sigs.k8s.io/yaml" 15 | ) 16 | 17 | func GetYaml(clusterName, group, version, resource, namespace, name string) *unstructured.Unstructured { 18 | cluster, _ := GetCluster(clusterName) 19 | dynamicClient := cluster.DynamicClient 20 | 21 | gvr := schema.GroupVersionResource{Group: group, Version: version, Resource: resource} 22 | unstructObj, _ := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{}) 23 | return unstructObj 24 | } 25 | 26 | func ApplyYaml(clusterName string, u *unstructured.Unstructured) *unstructured.Unstructured { 27 | gvk := u.GroupVersionKind() 28 | gvr, _ := FindGVR(clusterName, &gvk) 29 | 30 | yamlBytes, _ := yaml.Marshal(u) 31 | cluster, _ := GetCluster(clusterName) 32 | dynamicClient := cluster.DynamicClient 33 | //todo 判断不存在则创建,否则更新,细节参考kubectl apply的实现,这里如果存在会报错 34 | patch, err := dynamicClient.Resource(*gvr).Namespace(u.GetNamespace()).Patch(context.Background(), u.GetName(), types.ApplyPatchType, yamlBytes, metav1.PatchOptions{FieldManager: ""}) 35 | fmt.Println(err) 36 | return patch 37 | } 38 | 39 | // FindGVR 查询gvk对应的gvr 40 | func FindGVR(clusterName string, gvk *schema.GroupVersionKind) (*schema.GroupVersionResource, error) { 41 | cluster, _ := GetCluster(clusterName) 42 | config := cluster.Config 43 | 44 | dc, err := discovery.NewDiscoveryClientForConfig(config) 45 | if err != nil { 46 | return nil, err 47 | } 48 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) 49 | mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 50 | if err != nil { 51 | return nil, err 52 | } 53 | //meta.RESTScopeNamespace 54 | return &mapping.Resource, nil 55 | } 56 | 57 | func FindGVRs(clusterName, group, version, kind string) { 58 | cluster, _ := GetCluster(clusterName) 59 | config := cluster.Config 60 | 61 | dc, _ := discovery.NewDiscoveryClientForConfig(config) 62 | mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) 63 | gvk := schema.GroupVersionKind{Group: group, Version: version, Kind: kind} 64 | 65 | mapping, err := mapper.RESTMappings(gvk.GroupKind(), gvk.Version) 66 | fmt.Println(err) 67 | for _, restMapping := range mapping { 68 | fmt.Println(restMapping.Resource) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /service/configmap.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "kubernetes-admin-backend/proto" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | func ListConfigMap(clusterName, namespace string) []proto.ConfigMap { 14 | cluster, _ := GetCluster(clusterName) 15 | clientSet := cluster.ClientSet 16 | configMapList, _ := clientSet.CoreV1().ConfigMaps(namespace).List(context.Background(), metav1.ListOptions{}) 17 | 18 | configMaps := make([]proto.ConfigMap, 0, 10) 19 | for _, item := range configMapList.Items { 20 | createDays := time.Now().Sub(item.GetCreationTimestamp().Time).Hours() / 24 21 | configMap := proto.ConfigMap{Name: item.Name, Namespace: item.Namespace, Labels: item.Labels, Annotations: item.Annotations, 22 | Data: item.Data, Age: strconv.FormatFloat(createDays, 'f', -1, 64) + " days"} 23 | configMaps = append(configMaps, configMap) 24 | } 25 | return configMaps 26 | } 27 | 28 | func GetConfigMapByName(clusterName, namespace, name string) *unstructured.Unstructured { 29 | cluster, _ := GetCluster(clusterName) 30 | dynamicClient := cluster.DynamicClient 31 | gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} 32 | unstructObj, _ := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{}) 33 | 34 | return unstructObj 35 | } 36 | -------------------------------------------------------------------------------- /service/event.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | func ListEvent(clusterName, namespace string) *v1.EventList { 10 | cluster, _ := GetCluster(clusterName) 11 | clientSet := cluster.ClientSet 12 | eventList, _ := clientSet.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{}) 13 | return eventList 14 | } 15 | -------------------------------------------------------------------------------- /service/informers.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | v1 "k8s.io/api/apps/v1" 6 | "k8s.io/apimachinery/pkg/labels" 7 | "k8s.io/client-go/informers" 8 | "k8s.io/client-go/tools/cache" 9 | "k8s.io/klog" 10 | "time" 11 | ) 12 | 13 | // todo 14 | func Informers() { 15 | cluster, _ := GetCluster("default") 16 | clientSet := cluster.ClientSet 17 | informerFactory := informers.NewSharedInformerFactory(clientSet, 30*time.Second) 18 | deployInformer := informerFactory.Apps().V1().Deployments() 19 | informer := deployInformer.Informer() 20 | deployLister := deployInformer.Lister() 21 | informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 22 | AddFunc: onAdd, 23 | UpdateFunc: onUpdate, 24 | DeleteFunc: onDelete, 25 | }) 26 | stopper := make(chan struct{}) 27 | defer close(stopper) 28 | //启动Informer List & Watch 29 | informerFactory.Start(stopper) 30 | // 等待所有的Informer缓存同步 31 | informerFactory.WaitForCacheSync(stopper) 32 | deployments, _ := deployLister.Deployments("default").List(labels.Everything()) 33 | //编辑deploy列表 34 | for index, deployment := range deployments { 35 | fmt.Printf("%d -> %s\n", index, deployment.Name) 36 | } 37 | <-stopper 38 | } 39 | 40 | func onAdd(obj interface{}) { 41 | deployment := obj.(*v1.Deployment) 42 | klog.Infoln("add a deploy:", deployment) 43 | 44 | } 45 | 46 | func onUpdate(old, new interface{}) { 47 | oldDeploy := old.(*v1.Deployment) 48 | newDeploy := new.(*v1.Deployment) 49 | klog.Infoln("update deploy:", *oldDeploy.Spec.Replicas, *newDeploy.Spec.Replicas) 50 | 51 | } 52 | 53 | func onDelete(obj interface{}) { 54 | deployment := obj.(*v1.Deployment) 55 | klog.Infoln("delete a deploy:", deployment.Name) 56 | } 57 | -------------------------------------------------------------------------------- /service/namespace.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/klog" 8 | "kubernetes-admin-backend/proto" 9 | ) 10 | 11 | func GetNamespaces(clusterName string) ([]v1.Namespace, error) { 12 | ctx := context.Background() 13 | cluster, _ := GetCluster(clusterName) 14 | clientSet := cluster.ClientSet 15 | namespaceList, err := clientSet.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) 16 | if err != nil { 17 | klog.Error(err) 18 | return nil, err 19 | } 20 | return namespaceList.Items, nil 21 | } 22 | 23 | func CreateNamespace(clusterName string, ns proto.NameSpace) (*v1.Namespace, error) { 24 | ctx := context.Background() 25 | cluster, _ := GetCluster(clusterName) 26 | clientSet := cluster.ClientSet 27 | 28 | newNamespace, err := clientSet.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ 29 | ObjectMeta: metav1.ObjectMeta{ 30 | Name: ns.Name, 31 | Labels: ns.Labels, 32 | Annotations: ns.Annotations, 33 | }, 34 | }, metav1.CreateOptions{}) 35 | if err != nil { 36 | klog.Error(err) 37 | return nil, err 38 | } 39 | 40 | return newNamespace, nil 41 | } 42 | 43 | func DeleteNamespace(clusterName, nsName string) error { 44 | ctx := context.Background() 45 | cluster, _ := GetCluster(clusterName) 46 | clientSet := cluster.ClientSet 47 | deletePolicy := metav1.DeletePropagationForeground 48 | err := clientSet.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{ 49 | PropagationPolicy: &deletePolicy, 50 | }) 51 | return err 52 | } 53 | 54 | func UpdateNamespace(clusterName string, nameSpace proto.NameSpace) (*v1.Namespace, error) { 55 | ctx := context.Background() 56 | cluster, _ := GetCluster(clusterName) 57 | clientSet := cluster.ClientSet 58 | 59 | namespace := &v1.Namespace{ 60 | ObjectMeta: metav1.ObjectMeta{ 61 | Name: nameSpace.Name, 62 | Labels: nameSpace.Labels, 63 | Annotations: nameSpace.Annotations, 64 | }, 65 | } 66 | update, err := clientSet.CoreV1().Namespaces().Update(ctx, namespace, metav1.UpdateOptions{}) 67 | return update, err 68 | } 69 | -------------------------------------------------------------------------------- /service/pod.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "kubernetes-admin-backend/proto" 7 | ) 8 | 9 | func GetPods(clusterName string) []proto.Pod { 10 | cluster, _ := GetCluster(clusterName) 11 | clientSet := cluster.ClientSet 12 | podList, _ := clientSet.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{}) 13 | 14 | pods := make([]proto.Pod, 0, 20) 15 | for _, item := range podList.Items { 16 | containers := make([]proto.Container, 0, len(item.Status.ContainerStatuses)) 17 | for _, containerStatus := range item.Status.ContainerStatuses { 18 | container := proto.Container{Name: containerStatus.Name, Ready: containerStatus.Ready, RestartCount: int(containerStatus.RestartCount), 19 | Image: containerStatus.Image, ImageId: containerStatus.ImageID, ContainerId: containerStatus.ContainerID} 20 | containers = append(containers, container) 21 | } 22 | pod := proto.Pod{Name: item.Name, Namespace: item.Namespace, Status: string(item.Status.Phase), CreationTimestamp: item.CreationTimestamp.Time, 23 | Containers: containers, Labels: item.Labels, Annotations: item.Annotations, PodIp: item.Status.PodIP, NodeName: item.Spec.NodeName} 24 | pods = append(pods, pod) 25 | } 26 | 27 | return pods 28 | } 29 | -------------------------------------------------------------------------------- /service/svc.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "kubernetes-admin-backend/proto" 8 | ) 9 | 10 | func ListSvc(clusterName, namespace, label string) []proto.Svc { 11 | cluster, _ := GetCluster(clusterName) 12 | clientSet := cluster.ClientSet 13 | //TODO 将与svc同名的endpoint设置到proto.Svc 14 | /*endpointsList, _ := clientSet.CoreV1().Endpoints(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: label}) 15 | nameToEndPoints := make(map[string]v1.Endpoints) 16 | fmt.Println(endpointsList.Items) 17 | for _, item := range endpointsList.Items { 18 | item. 19 | 20 | }*/ 21 | 22 | serviceList, _ := clientSet.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: label}) 23 | services := serviceList.Items 24 | svcs := make([]proto.Svc, 0, 10) 25 | for _, service := range services { 26 | prots := make([]string, 0, 5) 27 | for _, servicePort := range service.Spec.Ports { 28 | if service.Spec.Type == v1.ServiceTypeNodePort { 29 | s := servicePort.TargetPort.StrVal + ":" + string(servicePort.NodePort) + "/" + string(servicePort.Protocol) 30 | prots = append(prots, s) 31 | } else { 32 | s := servicePort.TargetPort.StrVal + "/" + string(servicePort.Protocol) 33 | prots = append(prots, s) 34 | } 35 | } 36 | 37 | svc := proto.Svc{Name: service.Name, Namespace: service.Namespace, 38 | Type: string(service.Spec.Type), ClusterIp: service.Spec.ClusterIP, 39 | Ports: prots, Selector: service.Spec.Selector, Labels: service.Labels, Annotations: service.Annotations, 40 | } 41 | svcs = append(svcs, svc) 42 | } 43 | 44 | return svcs 45 | } 46 | -------------------------------------------------------------------------------- /terminal/terminal_pod.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/gorilla/websocket" 7 | v1 "k8s.io/api/core/v1" 8 | "k8s.io/client-go/kubernetes/scheme" 9 | "k8s.io/client-go/tools/remotecommand" 10 | "kubernetes-admin-backend/service" 11 | "net/http" 12 | "sync" 13 | ) 14 | 15 | //包级变量,升级器 16 | var upgrader = websocket.Upgrader{} 17 | 18 | func init() { 19 | //初始化 20 | upgrader = websocket.Upgrader{ 21 | ReadBufferSize: 1024, 22 | WriteBufferSize: 1024, 23 | // 解决跨域问题 24 | CheckOrigin: func(r *http.Request) bool { 25 | return true 26 | }, 27 | } 28 | } 29 | 30 | // websocket消息 31 | type WsMessage struct { 32 | MessageType int 33 | Data []byte 34 | } 35 | 36 | // 封装websocket连接 37 | type WsConnection struct { 38 | conn *websocket.Conn // 底层websocket 39 | inChan chan *WsMessage // 读取队列 40 | outChan chan *WsMessage // 发送队列 41 | mutex sync.Mutex // 避免重复关闭管道 42 | isClosed bool 43 | closeChan chan byte // 关闭通知 44 | } 45 | 46 | // 读取协程 47 | func (wsConn *WsConnection) wsReadLoop() { 48 | for { 49 | // 读一条message 50 | msgType, data, err := wsConn.conn.ReadMessage() 51 | if err != nil { 52 | break 53 | } 54 | 55 | // 放入请求队列 56 | wsConn.inChan <- &WsMessage{ 57 | msgType, 58 | data, 59 | } 60 | 61 | //select { 62 | //case wsConn.inChan <- msg: 63 | //case <- wsConn.closeChan: 64 | // 65 | //} 66 | } 67 | } 68 | 69 | // 发送协程 70 | func (wsConn *WsConnection) wsWriteLoop() { 71 | // 服务端返回给页面的数据 72 | 73 | for { 74 | var msg *WsMessage 75 | select { 76 | // 取一个应答 77 | case msg = <-wsConn.outChan: 78 | // 写给web websocket 79 | if err := wsConn.conn.WriteMessage(msg.MessageType, msg.Data); err != nil { 80 | break 81 | } 82 | case <-wsConn.closeChan: 83 | wsConn.WsClose() 84 | } 85 | } 86 | } 87 | 88 | func InitWebsocket(resp http.ResponseWriter, req *http.Request) (wsConn *WsConnection, err error) { 89 | // 应答客户端告知升级连接为websocket 90 | conn, err := upgrader.Upgrade(resp, req, nil) 91 | if err != nil { 92 | return 93 | } 94 | wsConn = &WsConnection{ 95 | conn: conn, 96 | inChan: make(chan *WsMessage, 1000), 97 | outChan: make(chan *WsMessage, 1000), 98 | closeChan: make(chan byte), 99 | isClosed: false, 100 | } 101 | 102 | // 页面读入输入 协程 103 | go wsConn.wsReadLoop() 104 | // 服务端返回数据 协程 105 | go wsConn.wsWriteLoop() 106 | 107 | return 108 | } 109 | 110 | // 发送返回消息到协程 111 | func (wsConn *WsConnection) WsWrite(messageType int, data []byte) (err error) { 112 | select { 113 | case wsConn.outChan <- &WsMessage{messageType, data}: 114 | 115 | case <-wsConn.closeChan: 116 | err = errors.New("WsWrite websocket closed") 117 | break 118 | } 119 | return 120 | } 121 | 122 | // 读取 页面消息到协程 123 | func (wsConn *WsConnection) WsRead() (msg *WsMessage, err error) { 124 | select { 125 | case msg = <-wsConn.inChan: 126 | return 127 | case <-wsConn.closeChan: 128 | err = errors.New("WsRead websocket closed") 129 | break 130 | } 131 | return 132 | } 133 | 134 | // 关闭连接 135 | func (wsConn *WsConnection) WsClose() { 136 | wsConn.conn.Close() 137 | wsConn.mutex.Lock() 138 | defer wsConn.mutex.Unlock() 139 | if !wsConn.isClosed { 140 | wsConn.isClosed = true 141 | close(wsConn.closeChan) 142 | } 143 | } 144 | 145 | // ssh流式处理器 146 | type streamHandler struct { 147 | wsConn *WsConnection 148 | resizeEvent chan remotecommand.TerminalSize 149 | } 150 | 151 | // web终端发来的包 152 | type xtermMessage struct { 153 | MsgType string `json:"type"` // 类型:resize客户端调整终端, input客户端输入 154 | Input string `json:"input"` // msgtype=input情况下使用 155 | Rows uint16 `json:"rows"` // msgtype=resize情况下使用 156 | Cols uint16 `json:"cols"` // msgtype=resize情况下使用 157 | } 158 | 159 | // executor回调获取web是否resize 160 | func (handler *streamHandler) Next() (size *remotecommand.TerminalSize) { 161 | ret := <-handler.resizeEvent 162 | size = &ret 163 | return 164 | } 165 | 166 | // executor回调读取web端的输入 167 | func (handler *streamHandler) Read(p []byte) (size int, err error) { 168 | 169 | // 读web发来的输入 170 | var msg *WsMessage 171 | if msg, err = handler.wsConn.WsRead(); err != nil { 172 | handler.wsConn.WsClose() 173 | return 174 | } 175 | // 解析客户端请求 176 | //if err = json.Unmarshal([]byte(msg.Data), &xtermMsg); err != nil { 177 | // return 178 | //} 179 | 180 | xtermMsg := &xtermMessage{ 181 | //MsgType: string(msg.MessageType), 182 | Input: string(msg.Data), 183 | } 184 | // 放到channel里,等remotecommand executor调用我们的Next取走 185 | handler.resizeEvent <- remotecommand.TerminalSize{Width: xtermMsg.Cols, Height: xtermMsg.Rows} 186 | size = len(xtermMsg.Input) 187 | copy(p, xtermMsg.Input) 188 | return 189 | 190 | } 191 | 192 | // executor回调向web端输出 193 | func (handler *streamHandler) Write(p []byte) (size int, err error) { 194 | // 产生副本 195 | copyData := make([]byte, len(p)) 196 | copy(copyData, p) 197 | size = len(p) 198 | err = handler.wsConn.WsWrite(websocket.TextMessage, copyData) 199 | return 200 | } 201 | 202 | func StartProcess(wsConn *WsConnection, clusterName string, podName string, namespace string, container string) error { 203 | cmd := []string{"/bin/sh"} 204 | // URL: 205 | // https://172.16.0.143:6443/api/v1/namespaces/default/pods/nginx-deployment-5cbd8757f-d5qvx/exec?command=sh&container=nginx&stderr=true&stdin=true&stdout=true&tty=true 206 | cluster, _ := service.GetCluster(clusterName) 207 | clientSet := cluster.ClientSet 208 | req := clientSet.CoreV1().RESTClient().Post(). 209 | Resource("pods"). 210 | Name(podName). 211 | Namespace(namespace). 212 | SubResource("exec"). 213 | VersionedParams(&v1.PodExecOptions{ 214 | Container: container, 215 | Command: cmd, 216 | Stdin: true, 217 | Stdout: true, 218 | Stderr: true, 219 | TTY: true, 220 | }, scheme.ParameterCodec) 221 | 222 | // 创建到容器的连接 223 | executor, err := remotecommand.NewSPDYExecutor(cluster.Config, "POST", req.URL()) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | // 配置与容器之间的数据流处理回调 229 | handler := &streamHandler{wsConn: wsConn, resizeEvent: make(chan remotecommand.TerminalSize)} 230 | if err = executor.Stream(remotecommand.StreamOptions{ 231 | Stdin: handler, 232 | Stdout: handler, 233 | Stderr: handler, 234 | TerminalSizeQueue: handler, 235 | Tty: true, 236 | }); err != nil { 237 | fmt.Println("handler", err) 238 | return err 239 | 240 | } 241 | return err 242 | 243 | } 244 | -------------------------------------------------------------------------------- /terminal/terminal_ssh.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "github.com/gorilla/websocket" 9 | "golang.org/x/crypto/ssh" 10 | "io" 11 | "time" 12 | ) 13 | 14 | type SSHClientConfig struct { 15 | AuthModel string 16 | HostAddr string 17 | User string 18 | Password string 19 | PublicKey string 20 | Timeout time.Duration 21 | } 22 | 23 | func NewSSHClient(conf *SSHClientConfig) (*ssh.Client, error) { 24 | config := &ssh.ClientConfig{ 25 | Timeout: conf.Timeout, 26 | User: conf.User, 27 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), //忽略know_hosts检查 28 | } 29 | switch conf.AuthModel { 30 | case "PASSWORD": 31 | config.Auth = []ssh.AuthMethod{ssh.Password(conf.Password)} 32 | case "PUBLICKEY": 33 | signer, err := ssh.ParsePrivateKey([]byte(conf.PublicKey)) 34 | if err != nil { 35 | return nil, err 36 | } 37 | config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} 38 | } 39 | c, err := ssh.Dial("tcp", conf.HostAddr, config) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return c, nil 44 | } 45 | 46 | type Turn struct { 47 | StdinPipe io.WriteCloser 48 | Session *ssh.Session 49 | WsConn *websocket.Conn 50 | } 51 | 52 | func NewTurn(wsConn *websocket.Conn, sshClient *ssh.Client) (*Turn, error) { 53 | sess, err := sshClient.NewSession() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | stdinPipe, err := sess.StdinPipe() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | turn := &Turn{StdinPipe: stdinPipe, Session: sess, WsConn: wsConn} 64 | sess.Stdout = turn 65 | sess.Stderr = turn 66 | 67 | modes := ssh.TerminalModes{ 68 | ssh.ECHO: 1, // disable echo 69 | ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 70 | ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 71 | } 72 | if err := sess.RequestPty("xterm", 150, 30, modes); err != nil { 73 | return nil, err 74 | } 75 | if err := sess.Shell(); err != nil { 76 | return nil, err 77 | } 78 | 79 | return turn, nil 80 | } 81 | 82 | func (t *Turn) Write(p []byte) (n int, err error) { 83 | writer, err := t.WsConn.NextWriter(websocket.TextMessage) 84 | if err != nil { 85 | return 0, err 86 | } 87 | defer writer.Close() 88 | fmt.Println("Write:" + string(p)) 89 | return writer.Write(p) 90 | } 91 | func (t *Turn) Close() error { 92 | fmt.Println("Close()") 93 | if t.Session != nil { 94 | t.Session.Close() 95 | } 96 | 97 | return t.WsConn.Close() 98 | } 99 | 100 | func (t *Turn) Read(p []byte) (n int, err error) { 101 | for { 102 | msgType, reader, err := t.WsConn.NextReader() 103 | if err != nil { 104 | return 0, err 105 | } 106 | if msgType != websocket.TextMessage { 107 | continue 108 | } 109 | fmt.Println("Write:" + string(p)) 110 | return reader.Read(p) 111 | } 112 | } 113 | 114 | func (t *Turn) LoopRead(context context.Context) error { 115 | for { 116 | select { 117 | case <-context.Done(): 118 | return errors.New("LoopRead exit") 119 | default: 120 | _, wsData, err := t.WsConn.ReadMessage() 121 | fmt.Println("本地输入:" + string(wsData)) 122 | if err != nil { 123 | return fmt.Errorf("reading webSocket message err:%s", err) 124 | } 125 | body := decode(wsData[1:]) 126 | fmt.Println("body:" + string(body)) 127 | body = wsData 128 | fmt.Println("body:" + string(body)) 129 | if _, err := t.StdinPipe.Write(body); err != nil { 130 | return fmt.Errorf("StdinPipe write err:%s", err) 131 | } 132 | 133 | /*switch wsData[0] { 134 | case MsgResize: 135 | var args Resize 136 | err := json.Unmarshal(body, &args) 137 | if err != nil { 138 | return fmt.Errorf("ssh pty resize windows err:%s", err) 139 | } 140 | if args.Columns > 0 && args.Rows > 0 { 141 | if err := t.Session.WindowChange(args.Rows, args.Columns); err != nil { 142 | return fmt.Errorf("ssh pty resize windows err:%s", err) 143 | } 144 | } 145 | case MsgData: 146 | if _, err := t.StdinPipe.Write(body); err != nil { 147 | return fmt.Errorf("StdinPipe write err:%s", err) 148 | } 149 | if _, err := logBuff.Write(body); err != nil { 150 | return fmt.Errorf("logBuff write err:%s", err) 151 | } 152 | }*/ 153 | } 154 | } 155 | } 156 | 157 | func (t *Turn) SessionWait() error { 158 | if err := t.Session.Wait(); err != nil { 159 | return err 160 | } 161 | return nil 162 | } 163 | 164 | func decode(p []byte) []byte { 165 | decodeString, _ := base64.StdEncoding.DecodeString(string(p)) 166 | return decodeString 167 | } 168 | -------------------------------------------------------------------------------- /test/cluster_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "k8s.io/client-go/kubernetes" 9 | "k8s.io/client-go/tools/clientcmd" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestAddCluster(t *testing.T) { 15 | bytes, err := ioutil.ReadFile("../config/.kube/default/config") 16 | if err != nil { 17 | fmt.Println("ReadFile," + err.Error()) 18 | } 19 | config, err := clientcmd.NewClientConfigFromBytes(bytes) 20 | if err != nil { 21 | fmt.Println("NewClientConfigFromBytes," + err.Error()) 22 | } 23 | clientConfig, err := config.ClientConfig() 24 | if err != nil { 25 | fmt.Println("ClientConfig," + err.Error()) 26 | } 27 | clientSet, err := kubernetes.NewForConfig(clientConfig) 28 | if err != nil { 29 | fmt.Println("NewForConfigorConfigFile," + err.Error()) 30 | } 31 | version, err := clientSet.ServerVersion() 32 | if err != nil { 33 | fmt.Println("ServerVersion," + err.Error()) 34 | } 35 | fmt.Println(version) 36 | 37 | } 38 | 39 | func TestFileToBytes(t *testing.T) { 40 | file, err := os.Open("../config/.kube/default/config") 41 | if err != nil { 42 | fmt.Println("Open," + err.Error()) 43 | } 44 | defer file.Close() 45 | buf := bytes.NewBuffer(nil) 46 | if _, err := io.Copy(buf, file); err != nil { 47 | fmt.Println("io.Copy," + err.Error()) 48 | } 49 | config, err := clientcmd.NewClientConfigFromBytes(buf.Bytes()) 50 | if err != nil { 51 | fmt.Println("NewClientConfigFromBytes," + err.Error()) 52 | } 53 | clientConfig, err := config.ClientConfig() 54 | if err != nil { 55 | fmt.Println("ClientConfig," + err.Error()) 56 | } 57 | clientSet, err := kubernetes.NewForConfig(clientConfig) 58 | if err != nil { 59 | fmt.Println("NewForConfigorConfigFile," + err.Error()) 60 | } 61 | version, err := clientSet.ServerVersion() 62 | if err != nil { 63 | fmt.Println("ServerVersion," + err.Error()) 64 | } 65 | fmt.Println(version) 66 | 67 | } 68 | --------------------------------------------------------------------------------