【云原生开发】K8S集群管理后端开发设计与实现_krm-backend 源码
✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:云原生开发
景天的主页:景天科技苑
文章目录
- K8S集群管理后端开发设计与实现
-
- 1、incluster命名空间的检测与创建
- 2、集群管理的路由配置
- 3、实现添加更新集群功能
- 4、 结构体转化成map工具包
- 5、删除集群
- 6、集群列表查询
- 7、获取集群详情
K8S集群管理后端开发设计与实现
集群管理包含在前端添加,更新,删除,查询所有集群,查询集群详情等功能实现。
1、incluster命名空间的检测与创建
根据之前的架构规划,我们的元数据存储在inCluster这个K8S集群中的krm名称空间,程序启动的时候,先检查krm这个命名空间是否存在,不存在的话我们就创建这个命名空间
设个默认值,并可以通过环境变量获取
检查命名空间需要在程序运行前执行,因此我们需要创建个controller,来做初始化检查操作
package initcontrollerimport ( _ \"jingtian/krm-backend/config\" //调用里面的init函数,将日志格式初始化 \"jingtian/krm-backend/utils/logs\")// 只写个init函数,用来检查配置func init() { //这里面需要初始化incluster的kubeconfig,创建客户端。创建元数据的命名空间 logs.Debug(nil, \"初始化incluster数据...\") // 1. 通过kubeconfig创建client-go客户端 // 2. 检查元数据命名空间是否创建,如有,提醒下元数据命名空间未创建。如没有,就创建命名空间 MetadataInit()}
在initcontroller包里面,创建个initcluster.go,用来检测命名空间是否创建,如未创建,即刻创建。
package initcontrollerimport ( \"context\" \"jingtian/krm-backend/config\" \"jingtian/krm-backend/utils/logs\" corev1 \"k8s.io/api/core/v1\" metav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\" \"k8s.io/client-go/kubernetes\" \"k8s.io/client-go/tools/clientcmd\")func MetadataInit() { logs.Debug(nil, \"初始化元数据命名空间\") // 1. 初始化config实例 // masterUrl就是离我们主节点的ip地址和端口号,我们在kubeconfig文件中有了,所以可以省略 kubeconfig, err := clientcmd.BuildConfigFromFlags(\"\", \"meta.kubeconfig\") //要想正常应用我们的服务,必须能够实例化成功kubeconfig,要不然后面所有的功能都无法使用,所以这里直接报panic即可 if err != nil { logs.Error(map[string]interface{}{\"msg\": err.Error()}, \"inCluster kubeconfig加载失败\") panic(err.Error()) } // 2. 创建客户端工具 create the clientset clientset, err := kubernetes.NewForConfig(kubeconfig) //这个客户端工具如果生成失败的话,后面的操作也无法完成,所以这里也报panic即可 if err != nil { logs.Error(map[string]interface{}{\"msg\": err.Error()}, \"inCluster客户端创建失败\") panic(err.Error()) } // 获取K8S的版本号 // ServerVersion() (*version.Info, error) inClusterVersion, _ := clientset.Discovery().ServerVersion() // 3.检查命名空间是否存在 _, err = clientset.CoreV1().Namespaces().Get(context.TODO(), config.MetadataNamespace, metav1.GetOptions{}) if err != nil { // 不存在元数据命名空间 logs.Info(nil, \"元数据命名空间不存在,准备创建....\") // 创建元数据命名空间 var metadataNamespace corev1.Namespace metadataNamespace.Name = config.MetadataNamespace _, err = clientset.CoreV1().Namespaces().Create(context.TODO(), &metadataNamespace, metav1.CreateOptions{}) if err != nil { logs.Error(map[string]interface{}{\"msg\": err.Error()}, \"元数据命名空间创建失败\") panic(err.Error()) } logs.Info(map[string]interface{}{\"Namespace\": config.MetadataNamespace, \"inCluster版本\": inClusterVersion.String()}, \"元数据命名空间创建成功\") } else { // 已经存在namespace logs.Info(map[string]interface{}{\"Namespace\": config.MetadataNamespace, \"inCluster版本\": inClusterVersion.String()}, \"元数据命名空间已存在\") }}
运行程序,元数据命名空间创建成功
在K8S集群查看命名空间,可见命名空间创建成功
2、集群管理的路由配置
我们得路由要在routers目录下去管理
并且通过routers.go文件实现路由的注册
创建集群管理的路由和控制器
routers.go
// Package routers 路由层 管理程序的路由信息package routersimport ( \"github.com/gin-gonic/gin\" \"jingtian/krm-backend/routers/auth\" \"jingtian/krm-backend/routers/cluster\")// RegisterRouters 需要将main.go里面的路由引擎r传过来// 写个注册路由的方法func RegisterRouters(r *gin.Engine) { //登录的路由配置 //1. 登录: login //2. 登出: loginout //3. 路由分组 /api/auth/login /api/auth/loginout apiGroup := r.Group(\"/api\") auth.RegisterSubRouter(apiGroup) cluster.RegisterSubRouter(apiGroup)}
cluster.go
package clusterimport ( \"github.com/gin-gonic/gin\" \"jingtian/krm-backend/controllers/cluster\")// 实现添加集群的接口func add(authGroup *gin.RouterGroup) { //具体逻辑写到控制器controller里面 authGroup.POST(\"/add\", cluster.Add)}// 实现更新集群的接口func update(authGroup *gin.RouterGroup) { //具体逻辑写到控制器controller里面 authGroup.POST(\"/update\", cluster.Update)}// 删除集群func deleteCluster(clusterGroup *gin.RouterGroup) { clusterGroup.GET(\"/delete\", cluster.DeleteCluster)}// 获取集群信息func get(clusterGroup *gin.RouterGroup) { clusterGroup.GET(\"/get\", cluster.Get)}// 列出所有集群func list(clusterGroup *gin.RouterGroup) { clusterGroup.GET(\"/list\", cluster.List)}// RegisterSubRouter 认证子路由func RegisterSubRouter(g *gin.RouterGroup) { //配置登录功能路由策略 clusterGroup := g.Group(\"/cluster\") add(clusterGroup) update(clusterGroup) deleteCluster(clusterGroup) get(clusterGroup) list(clusterGroup)}
控制器
由于除了登录和登出的其他接口,都要携带token才能访问,所以我们先登录,生成token,携带token发出请求
携带token请求测试
请求能走到添加集群的控制器
3、实现添加更新集群功能
怎么实现添加集群呢? 前端可能会让我们输入一些集群的信息,比如集群的名字,集群的ID,kubeconfig等。前端把这些信息提交给后端,然后后端再根据这些信息来创建集群
gin框架可以将前端传来的json数据和结构体进行绑定,然后通过结构体进行创建资源等其他操作。
我们先创建个结构体,来声明创建一个集群所需要的字段。我们将结构体的定义放在controller/cluster.go中
package clusterimport ( \"k8s.io/client-go/kubernetes\" \"k8s.io/client-go/tools/clientcmd\")// MyClusterInfo 主要用于查询操作type MyClusterInfo struct { Id string `json:\"id\"` DisplayName string `json:\"displayName\"` //集群的别名 City string `json:\"city\"` District string `json:\"district\"`}// MyClusterStatus 定义一个结构体,用于描述集群的状态// 集群可用,状态就是Active 不可用,状态就是InActivetype MyClusterStatus struct { MyClusterInfo Version string `json:\"version\"` Status string `json:\"status\"`}// MyClusterConfig 定义一个结构体,用于描述创建集群所用的配置信息type MyClusterConfig struct { MyClusterInfo Kubeconfig string `json:\"kubeconfig\"` //集群的配置资源}// GetClusterStatus 结构体的方法,用于判断集群的状态func (cfg *MyClusterConfig) GetClusterStatus() (MyClusterStatus, error) { // 判断集群是否是正常 clusterStatus := MyClusterStatus{} clusterStatus.MyClusterInfo = cfg.MyClusterInfo //此时,需要检测集群实时状态,所以需要重新连接集群 // 创建一个clientset,从前端传来的kubeconfig字符串来连接集群,这也是out-cluster方式创建客户端 // func RESTConfigFromKubeConfig(configBytes []byte) (*restclient.Config, error) // 接收的参数是字节类型 restConfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(cfg.Kubeconfig)) if err != nil { clusterStatus.Version = \"Unavailable Version\" clusterStatus.Status = \"InActive\" return clusterStatus, err } clientset, err := kubernetes.NewForConfig(restConfig) if err != nil { clusterStatus.Version = \"Unavailable Version\" clusterStatus.Status = \"InActive\" return clusterStatus, err } //客户端工具创建成功后,获取版本信息 serverVersion, err := clientset.Discovery().ServerVersion() if err != nil { return clusterStatus, err } //集群正常就将集群的版本和状态返回 clusterVersion := serverVersion.String() clusterStatus.Version = clusterVersion clusterStatus.Status = \"Active\" return clusterStatus, nil}
由于添加和更新集群,所用的参数差不多,所以我们将添加和更新写到一个函数中AddOrUpdate.go,根据传参,来区分是添加还是更新
package clusterimport ( \"context\" \"fmt\" \"github.com/gin-gonic/gin\" \"jingtian/krm-backend/config\" \"jingtian/krm-backend/utils\" \"jingtian/krm-backend/utils/logs\" corev1 \"k8s.io/api/core/v1\" metav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\" \"net/http\")// method: update, createfunc addOrUpdate(c *gin.Context, method string) { var arg string if method == \"Create\" { arg = \"添加\" } else if method == \"Update\" { arg = \"更新\" } // 声明一个集群配置 clusterConfig := MyClusterConfig{} returnData := config.NewReturnData() if err := c.ShouldBindJSON(&clusterConfig); err != nil { msg := arg + \"集群的配置信息不完整: \" + err.Error() returnData.Status = 400 returnData.Msg = msg c.JSON(200, returnData) return } // 判断集群是否正常,写个函数去连接集群,如果能获取到集群的版本,说明集群是正常的 // 这个函数写在哪呢,应该写在MyClusterConfig这个结构体的方法中 clusterStatus, err := clusterConfig.GetClusterStatus() if err != nil { msg := \"无法获取集群信息: \" + err.Error() returnData.Status = 400 returnData.Msg = msg c.JSON(http.StatusOK, returnData) logs.Error(map[string]interface{}{\"error\": err.Error()}, arg+\"集群失败,无法获取集群信息\") return } logs.Info(map[string]interface{}{\"集群名称\": clusterConfig.DisplayName, \"集群ID\": clusterConfig.Id}, \"开始\"+arg+\"集群\") // 创建一个集群配置的secret 保存集群信息 var clusterConfigSecret corev1.Secret clusterConfigSecret.Name = clusterConfig.Id clusterConfigSecret.Labels = make(map[string]string) clusterConfigSecret.Labels[config.ClusterConfigSecretLabelKey] = config.ClusterConfigSecretLabelValue // 添加注释,保存集群的配置信息 clusterConfigSecret.Annotations = make(map[string]string) // 把集群的状态结构体转成map。我们专门写个工具函数来实现结构体与map的转换 m, err := utils.Struct2Map(clusterStatus) if err != nil { logs.Error(nil, err.Error()) return } clusterConfigSecret.Annotations = m // 保存kubeconfig,我们保存到StringData里面的。我们点进去secret查看 //里面有Data和StringData两种,Data是需要加密的才能使用,StringData我们可以传字符串,当我们查看secret的时候自动加密 //Data map[string][]byte `json:\"data,omitempty\" protobuf:\"bytes,2,rep,name=data\"` //StringData map[string]string `json:\"stringData,omitempty\" protobuf:\"bytes,4,rep,name=stringData\"` //如果在 data 和 stringData 中设置了同一个字段,则使用来自 stringData 中的值 clusterConfigSecret.StringData = make(map[string]string) clusterConfigSecret.StringData[\"kubeconfig\"] = clusterConfig.Kubeconfig // 创建secret if method == \"Create\" { _, err = config.InClusterClientSet.CoreV1().Secrets(config.MetadataNamespace).Create(context.TODO(), &clusterConfigSecret, metav1.CreateOptions{}) } else if method == \"Update\" { //更新 _, err = config.InClusterClientSet.CoreV1().Secrets(config.MetadataNamespace).Update(context.TODO(), &clusterConfigSecret, metav1.UpdateOptions{}) } if err != nil { // 说明创建失败 logs.Error(map[string]interface{}{\"集群ID\": clusterConfig.Id, \"集群名字:\": clusterConfig.DisplayName, \"msg\": err.Error()}, \"集群\"+arg+\"失败\") msg := arg + \"集群失败: \" + err.Error() returnData.Msg = msg returnData.Status = 400 c.JSON(200, returnData) return } //创建or更新成功 //map需要先初始化 config.ClusterKubeconfig = make(map[string]string) config.ClusterKubeconfig[clusterConfig.Id] = clusterConfig.Kubeconfig fmt.Println(\"当前集群配置:\", config.ClusterKubeconfig) logs.Info(map[string]interface{}{\"集群ID\": clusterConfig.Id, \"集群名字:\": clusterConfig.DisplayName}, \"集群\"+arg+\"成功\") returnData.Status = 200 returnData.Msg = arg + \"成功\" c.JSON(200, returnData)}
add.go中
update.go中
4、 结构体转化成map工具包
结构体转换成map工具类utils/utils.go
// Package utils 工具层package utilsimport \"encoding/json\"func Struct2Map(s interface{}) (map[string]string, error) { j, _ := json.Marshal(s) //先将结构体转换成字节 m := make(map[string]string) err := json.Unmarshal(j, &m) //将字节转化成map if err != nil { return nil, err } return m, nil}
测试添加集群接口
看下绑定的结构体,来确定前端传的数据
postman请求
我们先不传kubeconfig,可以看到检测到集群不正常,无法添加
我们把集群的~/.kube/config填进去
注意,需要把换行符改成\\n。不然postman无法传参
再次请求
k8s集群查看secret,添加成功
查看详情
更新集群
只需要在请求处,将URL改成update,然后修改字段即可
5、删除集群
删除集群,只需要根据集群ID,将对应的secret删除掉即可
删除集群的控制器deleteCluster.go
package clusterimport ( \"context\" \"github.com/gin-gonic/gin\" \"jingtian/krm-backend/config\" \"jingtian/krm-backend/utils/logs\" metav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\")func DeleteCluster(c *gin.Context) { logs.Debug(nil, \"删除集群\") // 1.根据传来的id来删除secret clusterId := c.Query(\"id\") // 2.删除secret err := config.InClusterClientSet.CoreV1().Secrets(config.MetadataNamespace).Delete(context.TODO(), clusterId, metav1.DeleteOptions{}) // 定义响应格式 returnData := config.NewReturnData() if err != nil { logs.Error(map[string]interface{}{\"id\": clusterId, \"message\": err.Error()}, \"删除失败\") // 说明删除集群失败 msg := \"集群删除失败: \" + err.Error() returnData.Status = 400 returnData.Msg = msg } else { logs.Warning(map[string]interface{}{\"id\": clusterId}, \"删除成功\") // 说明删除成功 returnData.Status = 200 returnData.Msg = \"删除成功\" delete(config.ClusterKubeconfig, clusterId) } //响应给前端 c.JSON(200, returnData)}
请求
k8s查看,删除成功
6、集群列表查询
集群列表查询,就是查询所有的secret列表,将secret列表返回给前端就可以了
我们先通过postman创建几个secret,模拟几个集群
查看secret
我们把secret都查询出来,但是我们不要把secret的所有信息都传给前端,因为可能会不安全,所以尽量不要把kubeconfig传给前端
我们只需要传annotations里面的数据就行了
因此,我们在查询到secret列表时,需要处理下
查询的时候,可以过滤
//根据指定标签,过滤出我们添加的集群listOptions := metav1.ListOptions{ LabelSelector: config.ClusterConfigSecretLabelKey + \"=\" + config.ClusterConfigSecretLabelValue,}secretList, err := config.InClusterClientSet.CoreV1().Secrets(config.MetadataNamespace).List(context.TODO(), listOptions)
ListOptions里面可以根据LabelSelector或者FieldSelector等过滤
查询集群列表list.go完整代码
package clusterimport ( \"context\" \"github.com/gin-gonic/gin\" \"jingtian/krm-backend/config\" \"jingtian/krm-backend/utils/logs\" metav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\")func List(c *gin.Context) { logs.Debug(nil, \"列出集群列表\") //根据指定标签,过滤出我们添加的集群 listOptions := metav1.ListOptions{ LabelSelector: config.ClusterConfigSecretLabelKey + \"=\" + config.ClusterConfigSecretLabelValue, } secretList, err := config.InClusterClientSet.CoreV1().Secrets(config.MetadataNamespace).List(context.TODO(), listOptions) returnData := config.NewReturnData() if err != nil { logs.Info(map[string]interface{}{\"message\": err.Error()}, \"查询集群列表失败\") // 查询失败 msg := \"查询失败: \" + err.Error() returnData.Status = 400 returnData.Msg = msg c.JSON(200, returnData) return } // 优化数据返回的结构,将annotions里面的map以切片的形式传给前端 var clusterList []map[string]string for _, v := range secretList.Items { anno := v.Annotations clusterList = append(clusterList, anno) } returnData.Msg = \"查询成功\" returnData.Data[\"items\"] = clusterList c.JSON(200, returnData)}
postman请求,拿到集群列表
7、获取集群详情
我们查询一个集群的配置的时候,需要用到get,还有就是编辑一个集群的时候,也需要用到查询集群的详情。
我们可以根据集群id来获取到secret,返回给前端
控制器get.go
package clusterimport ( \"context\" \"fmt\" \"github.com/gin-gonic/gin\" \"jingtian/krm-backend/config\" \"jingtian/krm-backend/utils/logs\" metav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\" \"net/http\")func Get(c *gin.Context) { logs.Debug(nil, \"获取集群详情\") // 1.根据传来的id来获取集群详情 clusterId := c.Query(\"id\") returnData := config.NewReturnData() // 2.获取集群的详细信息 secretDetail, err := config.InClusterClientSet.CoreV1().Secrets(config.MetadataNamespace).Get(context.TODO(), clusterId, metav1.GetOptions{}) if err != nil { logs.Error(map[string]interface{}{\"id\": clusterId, \"message\": err.Error()}, \"获取集群信息失败\") returnData.Status = 400 returnData.Msg = \"获取集群详情失败 \" + err.Error() } else { returnData.Msg = \"集群详情查询成功\" fmt.Println(\"secretDetail是什么\", secretDetail) clusterConfigMap := secretDetail.Annotations // Data map[string][]byte `json:\"data,omitempty\" protobuf:\"bytes,2,rep,name=data\"` Data 键是字符串,值是字节 // 通过string将值转化成字符串 //获取的时候不可以用StringData,因为secret里面没有这个字段。只有Data字段 clusterConfigMap[\"kubeconfig\"] = string(secretDetail.Data[\"kubeconfig\"]) returnData.Data[\"item\"] = clusterConfigMap } c.JSON(http.StatusOK, returnData)}
postman请求,查询到集群详情数据