简介

本章节主要讲解通过client-go实现读取pod的列表,并实现将通过实现web终端登录pod容器内部执行命令和查看文件,实现将pod stdout的日志加载到浏览器进行查看。

一.读取k8spod列表功能

1.1.controllers控制器代码

该列表可以通过传递节点名称、deployment名称、标签、集群名称、命名空间来进行查询pod,在controlers目录下新建pod.go,代码参考如下:

func (this *PodController) List() {
	clusterId := this.GetString("clusterId") //集群ID
	nameSpace := this.GetString("nameSpace") //命名空间
	deployName := this.GetString("deployName") //deployment名称
	podName := this.GetString("podName") //pod名称
	nodeName := this.GetString("nodeName") //节点名称
	labels := this.GetString("labels") //标签,格式:key:value
	labelsKV := strings.Split(labels, ":") //yaml里没有对应的appname,deployment不会传递过来
	var labelsKey, labelsValue string
	if len(labelsKV) == 2 {
		labelsKey = labelsKV[0]
		labelsValue = labelsKV[1]
	}
	//从models的PodList函数进行读取数据
	podList, err := m.PodList(clusterId, nameSpace, deployName, podName, labelsKey, labelsValue, nodeName)
	msg := "success"
	code := 0
	if err != nil {
		log.Println(err)
		code = -1
		msg = err.Error()
	}

	count := len(podList)
	this.Data["json"] = &map[string]interface{}{"code": code, "msg": msg, "count": count, "data": &podList}
	this.ServeJSON()
}

1.2.models模型代码

在处理pod信息的过程中,需要两个结构体,Podinfo是pod信息,Container是pod中Container的结构体信息, 在models目录下新建podModel.go,代码参考如下:

type Podinfo struct {
	PodName   string `json:"podName"` //podname 和podip
	PodIp     string `json:"podIp"`
	NameSpace string `json:"nameSpace"`
	NodeName  string `json:"nodeName"` //节点名称和节点IP
	HostIp    string `json:"hostIp"`
	PodPhase  string `json:"podPhase"` //容器状态 Running
	ImgUrl    string `json:"imgUrl"`    //镜像地址
	//PodStatus    PodStatusItem
	RestartCount int32  `json:"restartCount"` //重启次数
	Labels       string `json:"labels"`       //标签
	ResStatus    string `json:"resStatus"`    //cpu及内存使用率
	CreateTime   string `json:"createTime"`
}

type Container struct {
	ContainerName  string `json:"containerName"` //container 名称
	Envs           string `json:"envs"` //设置的变量
	Mounts         string `json:"mounts"` //挂载路径
	ContainerImage string `json:"containerImage"` //镜像地址
	PullPolicy     string `json:"pullPolicy"` //镜像拉取策略
	Ports          string `json:"ports"` //端口
	ResLimits      string `json:"resLimits"` //限制的资源
	ResRequests    string `json:"resRequests"` //请求的资源
}

func PodList(kubeconfig, namespace, deployName, podName string, labelsKey, labelsValue, nodeName string) ([]Podinfo, error) {

	if namespace == "" {
		//namespace = corev1.NamespaceDefault
		namespace = corev1.NamespaceAll
	}

	clientset := common.ClientSet(kubeconfig)
	var podList *corev1.PodList
	var err error

	//设置ListOptions
	var listOptions = metav1.ListOptions{}
	if labelsKey != "" && labelsValue != "" {
		listOptions = metav1.ListOptions{
			LabelSelector: fmt.Sprintf("%s=%s", labelsKey, labelsValue),
			//FieldSelector: "status.phase=Running,spec.nodeName=ais-master1",
		}
	}

	if nodeName != "" {
		listOptions = metav1.ListOptions{
			FieldSelector: fmt.Sprintf("status.phase!=Succeeded,spec.nodeName=%s", nodeName),
		}
	}

	podList, err = clientset.CoreV1().Pods(namespace).List(context.Background(), listOptions)

	if err != nil {
		log.Printf("list pods error:%v\n", err)
	}
	var bbb = make([]Podinfo, 0)
	//循环处理pod列表的信息,并追加到结构体数组中
	for _, pod := range podList.Items {
		//搜索
		if podName != "" {
			if !strings.Contains(pod.Name, podName) {
				//if !strings.HasPrefix(pod.Name,podName)
				continue
			}
		}
		if deployName != "" {
			if !strings.HasPrefix(pod.Name, deployName) {
				continue
			}
		}

		var labelsStr, imgurlStr string
		for kk, vv := range pod.ObjectMeta.Labels {
			labelsStr += fmt.Sprintf("%s:%s,", kk, vv)
		}
		if len(labelsStr) > 0 {
			labelsStr = labelsStr[0 : len(labelsStr)-1]
		}

		var containerState = fmt.Sprintf("%v", pod.Status.Phase)

		var restartNum int32
		if len(pod.Status.ContainerStatuses) > 0 {
			imgurlStr = pod.Status.ContainerStatuses[0].Image
			restartNum = pod.Status.ContainerStatuses[0].RestartCount
			if containerState == "Pending" {
				containerState = pod.Status.ContainerStatuses[0].State.Waiting.Reason
			}
		}

		Items := &Podinfo{
			PodName:      pod.Name,
			PodIp:        pod.Status.PodIP,
			ImgUrl:       imgurlStr,
			NameSpace:    pod.ObjectMeta.Namespace,
			NodeName:     pod.Spec.NodeName,
			HostIp:       pod.Status.HostIP,
			PodPhase:     containerState,
			Labels:       labelsStr,
			RestartCount: restartNum,
			CreateTime:   pod.ObjectMeta.CreationTimestamp.Format("2006-01-02 15:04:05"),
		}
		bbb = append(bbb, *Items)
	}
	return bbb, err
}

二.routers路由配置

路由配置,配置URL指向到控制器的函数,将这段代码添加到routes/route.go,

	beego.Router("/pod/v1/List", &controllers.PodController{}, "*:List")    //pod列表
	beego.Router("/pod/v1/ContainerList", &controllers.PodController{}, "*:ContainerList")  //读取pod中container的列表
	beego.Router("/pod/v1/Log", &controllers.PodController{}, "*:Log")  //读取日志
	beego.Handler("/pod/terminal/ws", &controllers.TerminalSockjs{}, true) //pod终端

三.pod的终端登录功能

3.1.controllers控制器代码

由于pod终端采用websocket来进行交互,代码中部分代码有参考了别人的,所以部分代码完整拷贝了过来,此部分功能无模型部分代码,调度的控制器在controllers下新建文件pod_terminal_websocket.go完整代码如下

// pod_terminal_websocket.go
package controllers

import (
	"encoding/json"
	"fmt"
	"net/http"

	"myk8s/common"

	beegolog "github.com/beego/beego/v2/core/logs"
	"gopkg.in/igm/sockjs-go.v2/sockjs"
	"k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/remotecommand"
)

func (self TerminalSockjs) Read(p []byte) (int, error) {
	var reply string
	var msg map[string]uint16
	reply, err := self.conn.Recv()
	if err != nil {
		return 0, err
	}
	if err := json.Unmarshal([]byte(reply), &msg); err != nil {
		return copy(p, reply), nil
	} else {
		self.sizeChan <- &remotecommand.TerminalSize{
			msg["cols"],
			msg["rows"],
		}
		return 0, nil
	}
}

func (self TerminalSockjs) Write(p []byte) (int, error) {
	err := self.conn.Send(string(p))
	return len(p), err
}

type TerminalSockjs struct {
	conn      sockjs.Session
	sizeChan  chan *remotecommand.TerminalSize
	context   string
	clusterId string
	namespace string
	pod       string
	container string
}

// 实现tty size queue
func (self *TerminalSockjs) Next() *remotecommand.TerminalSize {
	size := <-self.sizeChan
	beegolog.Debug(fmt.Sprintf("terminal size to width: %d height: %d", size.Width, size.Height))
	return size
}

// 处理输入输出与sockjs 交互
func Handler(t *TerminalSockjs, cmd string) error {
	//restclient, config := common.RestClient(t.clusterId)
	clientset, config := common.ClientSetConfig(t.clusterId)
	req := clientset.CoreV1().RESTClient().Post().
		//req := restclient.Post().
		Resource("pods").
		Name(t.pod).
		Namespace(t.namespace).
		SubResource("exec").
		Param("container", t.container).
		Param("stdin", "true").
		Param("stdout", "true").
		Param("stderr", "true").
		Param("command", cmd).Param("tty", "true")
	req.VersionedParams(
		&v1.PodExecOptions{
			Container: t.container,
			Command:   []string{},
			Stdin:     true,
			Stdout:    true,
			Stderr:    true,
			TTY:       true,
		},
		scheme.ParameterCodec,
	)
	executor, err := remotecommand.NewSPDYExecutor(
		config, http.MethodPost, req.URL(),
	)
	if err != nil {
		return err
	}
	return executor.Stream(remotecommand.StreamOptions{
		Stdin:             t,
		Stdout:            t,
		Stderr:            t,
		Tty:               true,
		TerminalSizeQueue: t,
	})
}

// 实现http.handler 接口获取入参
func (self TerminalSockjs) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	context := r.FormValue("context")
	clusterId := r.FormValue("clusterId")
	namespace := r.FormValue("nameSpace")
	pod := r.FormValue("podName")
	container := r.FormValue("container")
	Sockjshandler := func(session sockjs.Session) {
		t := &TerminalSockjs{session, make(chan *remotecommand.TerminalSize),
			context, clusterId, namespace, pod, container}
		if err := Handler(t, "/bin/sh"); err != nil {
			beegolog.Error(err)
			beegolog.Error(Handler(t, "/bin/bash"))
		}
	}

	sockjs.NewHandler("/terminal/ws", sockjs.DefaultOptions, Sockjshandler).ServeHTTP(w, r)
}

四.pod的日志功能

4.1.controllers控制器代码

该部分代码在pod.go中,根据传入集群名称,命名空间,pod的名称来读取日志,由于在读取日志时是从具体的container中去读取,所以需要加载container列表并选择container来读取日志,代码参考如下

//加载container列表
func (this *PodController) ContainerList() {
	clusterId := this.GetString("clusterId")
	nameSpace := this.GetString("nameSpace")
	podName := this.GetString("podName")
	code := 0
	msg := "success"
	xList, err := m.PodContainerList(clusterId, nameSpace, podName)
	if err != nil {
		code = -1
		msg = err.Error()
		log.Printf("[ERROR] ContainerList error:%s\n", err)
	}
	this.Data["json"] = &map[string]interface{}{"code": code, "msg": msg, "count": len(xList), "data": &xList}
	this.ServeJSON()
}

//读取日志
func (this *PodController) Log() {
	clusterId := this.GetString("clusterId")
	nameSpace := this.GetString("nameSpace")
	podName := this.GetString("podName")
	download, _ := this.GetBool("download")
	container := this.GetString("container")
	logLine, _ := this.GetInt64("logLine")

	if this.Ctx.Input.Method() == "POST" {
		gp := gjson.ParseBytes(this.Ctx.Input.RequestBody)
		podName = gp.Get("podName").String()
		logLine = gp.Get("logLine").Int()
		container = gp.Get("container").String()
	}
	if logLine == 0 { //没设置行时,默认为100
		logLine = 100
	}
	if download { //当日志时下载时,不限制行数
		logLine = 0
	}
	log := m.PodLog(clusterId, nameSpace, podName, container, logLine)
	this.Ctx.WriteString(log)
}

4.2.models模型代码

PodContainerList函数是读取pod中的container,PodLog函数是读取日志。

func PodContainerList(kubeconfig, nameSpace, podName string) ([]Container, error) {
	var ccc = make([]Container, 0)
	pod, err := common.ClientSet(kubeconfig).CoreV1().Pods(nameSpace).Get(context.TODO(), podName, metav1.GetOptions{})
	if err != nil {
		return ccc, err
	}
	for _, v1 := range pod.Spec.Containers {
		xItems := &Container{
			ContainerName:  v1.Name,
			Envs:           "",
			Mounts:         "",
			ContainerImage: v1.Image,
			PullPolicy:     fmt.Sprintf("%v", v1.ImagePullPolicy),
			Ports:          "",
			ResLimits:      fmt.Sprintf("CPU:%s,Memory:%s", v1.Resources.Limits.Cpu().String(), v1.Resources.Limits.Memory().String()),
			ResRequests:    fmt.Sprintf("CPU:%s,Memory:%s", v1.Resources.Requests.Cpu().String(), v1.Resources.Requests.Memory().String()),
		}
		ccc = append(ccc, *xItems)
	}
	return ccc, nil
}

func PodLog(kubeconfig, nameSpace, podName, container string, logLine int64) string {
	clientset := common.ClientSet(kubeconfig)
	pod, err := clientset.CoreV1().Pods(nameSpace).Get(context.TODO(), podName, metav1.GetOptions{})
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get pod %q: %v\n", podName, err)
		//os.Exit(1)
	}
	// 获取指定行数日志
	//logs, err := clientset.CoreV1().Pods(nameSpace).GetLogs(podName, &corev1.PodLogOptions{TailLines: &lines})
	//获取实时日志
	var logOptions = &corev1.PodLogOptions{}
	logOptions.Follow = false     //持续输出
	logOptions.Timestamps = false //显示时间戳
	//var line int64 = 50
	if logLine > 0 {
		//line := logLine
		logOptions.TailLines = &logLine //获取多少行日志 需要指针类型https://blog.csdn.net/weixin_36469852/article/details/127432931
	}

	var containerPt *string = &container
	if container == "" {
		// [CONTAINER] (container as arg not flag) is supported as legacy behavior. See PR #10519 for more details.
		if len(pod.Spec.Containers) != 1 {
			podContainersNames := []string{}
			for _, container := range pod.Spec.Containers {
				podContainersNames = append(podContainersNames, container.Name)
			}
		} else {
			containerPt = &pod.Spec.Containers[0].Name
		}
	}
	logOptions.Container = *containerPt //指定container来获取日志,需要指针类型https://blog.csdn.net/weixin_36469852/article/details/127432931

	podLogs, err := clientset.CoreV1().Pods(nameSpace).GetLogs(podName, logOptions).Stream(context.TODO())
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get logs for pod %q: %v\n", podName, err)
		//os.Exit(1)
		return "error"
	}
	defer podLogs.Close()

	//单次输出
	buf := new(bytes.Buffer)
	_, err = io.Copy(buf, podLogs)
	if err != nil {
		return "error copy"
	}
	return buf.String()

	//流式输出日志内容
	// buf := make([]byte, 1024)
	// for {
	// 	n, err := podLogs.Read(buf)
	// 	if err != nil && err == io.EOF {
	// 		break
	// 	}
	// 	fmt.Print(string(buf[0:n]))
	// }
	// return "test"
}

五.前端html代码实现

5.1.pod列表html代码

前端部分html代码:在views\front\page\xkube 创建一个podList.html【pod列表】,podLog.html【pod日志】,podTerminal.html【pod终端】文件,其中podTerminal.html中会调用到:bootstrap.min.js,sockjs.min.js,bootstrap.min.css,xterm.css可以在网上下载来将js放到js目录下,css文件放到css目录下,或者 html代码如下: podList.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>pod列表</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="/lib/layui-v2.6.3/css/layui.css" media="all">
    <link rel="stylesheet" href="/css/public.css" media="all">
    <script type="text/javascript" src="/lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
    <script src="/lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
    <script src="/js/xkube.js?v=1" charset="utf-8"></script>
    <script src="/js/lay-config.js?v=1.0.4" charset="utf-8"></script>
<style type="text/css">
  .layui-table-cell {
    height: auto;
    line-height: 15px !important;
    text-overflow: inherit;
    overflow: ellipsis;
    white-space: normal;
  }
  .layui-table-cell .layui-table-tool-panel li {
    word-break: break-word;
  }
</style>
</head>
<body>
<div class="layuimini-container">
    <div class="layuimini-main">
        <table class="layui-table" id="currentTableId" lay-filter="currentTableFilter"></table>

        <script type="text/html" id="currentTableBar">
            <a class="layui-btn layui-btn-sm" lay-event="podLog">日志</a>
            <a class="layui-btn layui-btn-normal layui-btn-sm" lay-event="terminal">终端</a>
        </script>
    </div>
</div>
</body>

<script type="text/html" id="TagTpl">
    {{# if (d.labels != "") { }}
            {{# layui.each(d.labels.split(','), function(index, item){ }}
                {{# if(index == 0) { }}
                        <span>{{ item }}</span>
                {{# }else{ }}
                        <br><span>{{ item }}</span>
                {{# } }}  
            {{# });  }}
    {{# }else{  }}
            <span></span>
    {{# } }}
</script>	

<script type="text/html" id="podPhaseTpl">
  {{# if ( d.podPhase == 'Running' ) { }}
     <span style="color:#218868">{{ d.podPhase}}</span>
  {{# } else if ( d.podPhase == 'Succeeded' ) { }}
     <span style="color:#1E9FFF">{{ d.podPhase}}</span>
	{{# } else { }}
    <span style="color:#FF5722">{{ d.podPhase}}</span>
{{# } }}
</script>

<script>
var clusterId = getQueryString("clusterId");
if (clusterId == null) {
	clusterId = getCookie("clusterId")
}
    layui.use(['form', 'table','miniTab'], function () {
        var $ = layui.jquery,
            form = layui.form,
            table = layui.table;
            miniTab = layui.miniTab,
            miniTab.listen();

            form.render();

        table.render({
            elem: '#currentTableId',
            url: '/pod/v1/List?clusterId='+clusterId,
            toolbar: '#toolbarDemo',
            defaultToolbar: ['filter', 'exports', 'print', {
                title: '提示',
                layEvent: 'LAYTABLE_TIPS',
                icon: 'layui-icon-tips'
            }],
            parseData: function(res) { //实现加载全部数据后再分页
            	if(this.page.curr) {
            		result=res.data.slice(this.limit*(this.page.curr-1),this.limit*this.page.curr);
            	}else{
            	  result=res.data.slice(0,this.limit);
              }
              return {
              	"code": res.code,
              	"msg":'',
              	"count":res.count,
              	"data":result
              };
            },
            cols: [[
                //{type: "checkbox", width: 50},
                {field: 'podName',title: '名称'},
                {field: 'imgUrl',title: '镜像'},
                {field: 'podPhase', title: '状态',sort: true,templet: '#podPhaseTpl'},   
                {field: 'nameSpace', title: '命名空间', sort: true},
                {field: 'restartCount',title: '重启',sort: true},                          
                {field: 'podIp',title: 'podIp',sort: true},
                {field: 'hostIp',title:'节点IP'},  
                {field: 'nodeName',title:'节点名称',edit:true,hide:true},    
                {field: 'labes', title: '标签',edit:true,hide:true, sort: true,templet: '#TagTpl'},           
                {field: 'createTime',hide:true, title: '创建时间'},
                {title: '操作', minWidth:420, toolbar: '#currentTableBar', align: "center"}
            ]],
            //done: function(res, curr, count) {
            //    tableMerge.render(this)
            //},
            //size:'lg',
            limits: [25, 50, 100],
            limit: 25,
            page: true
        });

       table.on('tool(currentTableFilter)', function (obj) {
            var data = obj.data;
            if (obj.event === 'podLog') {
                var index = layer.open({
                    title: '日志',
                    type: 2,
                    shade: 0.2,
                    maxmin:true,
                    shadeClose: true,
                    area: ['55%', '92%'],
                    content: '/page/xkube/podLog.html?clusterId='+clusterId+'&nameSpace='+data.nameSpace+"&podName="+data.podName,
                });
                $(window).on("resize", function () {
                    layer.full(index);
                });
                return false;
            } else if (obj.event === 'terminal') {
                var index = layer.open({
                    title: '终端',
                    type: 2,
                    shade: 0.2,
                    maxmin:true,
                    shadeClose: true,
                    area: ['55%', '92%'],
                    content: '/page/xkube/podTerminal.html?clusterId='+clusterId+'&nameSpace='+data.nameSpace+"&podName="+data.podName,
                });
                $(window).on("resize", function () {
                    layer.full(index);
                });
                return false;
            }
        });

    });
</script>
</html>

5.2.pod日志查看html代码

podLog.html的代码,放在views\front\page\xkube下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>pod日志</title>
    <meta name="renderer" content="webkit">
    <link rel="stylesheet" href="/lib/layui-v2.6.3/css/layui.css" media="all">
    <link rel="stylesheet" href="/css/public.css" media="all">
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <script src="/js/xkube.js?v=1" charset="utf-8"></script>
    <script type="text/javascript" src="/lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
</head>
<body>
<div class="layuimini-container">
    <div class="layuimini-main">
                <form class="layui-form layui-form-pane" action="">
                    <div class="layui-form-item">
                        <div class="layui-inline">
                            <div class="layui-input-inline" style="width:200px"> 
                              <select name="podName" id="pod_Name" lay-filter="pod_Name">
                		           </select>
                            </div>
                        </div>
                        <div class="layui-inline">
                            <div class="layui-input-inline" style="width:150px"> 
                                <select name="container" id="container" lay-filter="container">
                		            </select>
                            </div>
                        </div>
                        <div class="layui-inline">
                            <div class="layui-input-inline" style="width:100px"> 
                                <select name="logLine" id="logLine">
            	              			<option value="" selected="">行数</option>
            	              			<option value="50">50</option>
            	              			<option value="100">100</option>
            	              			<option value="200">200</option>
                                  <option value="500">500</option>
                		            </select>
                            </div>
                        </div>
                        <div class="layui-inline">
                            <button type="submit" class="layui-btn"  lay-submit lay-filter="ReloadLog"><i class="layui-icon">&#xe669;</i>刷新</button>
                            <button class="layui-btn layui-btn-normal" id="DownLoadBtn">下载日志</button>
                        </div>
                    </div>
                </form>
      <pre class="layui-code"><div id="logtext"></div></pre>  
    </div>
</div>
<script src="/lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script>
function getQueryString(name) {
  let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
  let r = window.location.search.substr(1).match(reg);
  if (r != null) {
      return unescape(r[2]);
  };
  return null;
};


    layui.use(['form', 'table','code'], function () {
        var $ = layui.jquery;
        var form = layui.form;

        layui.code({
            title: 'pod日志'
            ,skin: 'notepad' //如果要默认风格,不用设定该key。
        });

        var deployName = getQueryString("deployName");
        var podName = getQueryString("podName");
        $('#appNameTitle').html(deployName);
        
  			  $.get('/pod/v1/List' + location.search, function (resp) {
    				$.each(resp.data,function(i,item){
              if (item.podName == podName ) {
      					   var html1 = '<option value="'+item.podName+'" selected>'+item.podName+'</option>'
              }else {
                  var html1 = '<option value="'+item.podName+'">'+item.podName+'</option>'
              }
      					$("#pod_Name").append(html1);
    				});
    			  //form.render('select', 'pod_Name');
    				form.render('');				
  		    });	

  			  $.get('/pod/v1/ContainerList' + location.search, function (resp) {
    				$.each(resp.data,function(i,item){
              if (i == 0 ) {
      					   var html1 = '<option value="'+item.containerName+'" selected>'+item.containerName+'</option>'
              }else {
                  var html1 = '<option value="'+item.containerName+'">'+item.containerName+'</option>'
              }
      					$("#container").append(html1);
    				});
    				form.render('');				
  		    });	

         $.ajax({
           url: "/pod/v1/Log"+location.search+"&logLine=50",
           type: "GET",
           success: function (resp) {
              $('#logtext').html(resp);
              window.scrollTo(0, document.body.scrollHeight);
            }
         });	

        //添加
        form.on('submit(ReloadLog)', function(datas){	
    		    console.log(JSON.stringify(datas.field)); 
             $.ajax({
               url: "/pod/v1/Log"+location.search,
               type: "POST",
               data: JSON.stringify(datas.field),
               success: function (resp) {
                  $('#logtext').html(resp);
                  window.scrollTo(0, document.body.scrollHeight);
                }
             });		  	                      	   
    	      return false; 	
    		});

        $('#DownLoadBtn').on("click",function(){
             $.ajax({
               url: "/pod/v1/Log"+location.search,
               type: "GET",
               success: function (resp) {
                  //$('#logtext').html(resp);
                  var xName = getQueryString("podName");
                  ExportRaw(xName+'.log',resp);
                }
             });	
          return false
        });

    });
</script>

</body>
</html>

5.3.pod终端html代码

podTerminal.html的代码,放在views\front\page\xkube下

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>pod终端</title>
    <meta name="renderer" content="webkit">
    <!--
    <link rel="stylesheet" href="/lib/layui-v2.6.3/css/layui.css" media="all">
    <link rel="stylesheet" href="/css/public.css" media="all">
    -->
    <link rel="stylesheet" href="/css/xterm.css" />
    <link rel="stylesheet" href="/css/bootstrap.min.css" />
    <script src="/js/xterm.js"></script>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <script type="text/javascript" src="/lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
    <script src="/js/bootstrap.min.js"></script>
    <!-- <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> -->
    <script src="/js/sockjs.min.js"></script>
    <style>
        body {
            color: #111;
            margin: 20px;
        }

        #terminal-container{
            margin: 0 auto;
        }
        #connect {
            margin: 0 auto;
        }
        #terminal-container a {
            color: #fff;
        }
        .panel-body{
            background-color: #000;
        }
        .xterm-rows {
            color: #e9e7e7;
            font-size: 14px;
        }
    </style>
</head>
<body style="border-width: 0px;margin:0px;height:100%;">

            <div style="padding: 0px;border: 0px;margin: 0px">
                <div id="terminal-container"></div>
            </div>
<script src="/lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script>
function getQueryString(name) {
  let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
  let r = window.location.search.substr(1).match(reg);
  if (r != null) {
      return unescape(r[2]);
  };
  return null;
};
     $(document).ready(function(){
         ws_connect();
     });

     // 获取宽度和高度
     console.log(document.body.clientWidth)
      console.log(document.documentElement.clientHeight)
     cols=parseInt(document.body.clientWidth /9)
     rows=parseInt(document.documentElement.clientHeight / 20)
     console.log(rows,cols)
     // 定义term对象
     var term = new Terminal({
         "backgroundColor":'#fff',
         "cursorBlink":true,
         "rows":rows,
         "cols":cols,
     });

    // 定义ws链接
    function ws_connect(){
        var socket
        // 隐藏连接按钮显示断开按钮
        $("#connect_container").hide()
        $("#drop_container").show()
        h=$("input[name=h]").val()
        p=$("input[name=p]").val()
        // 获取容器id
        containers_id=$("input[name=containers_id]").val()
        // 打印容器id
        console.log(h,p,containers_id)
        if( h == "" || p == "" || containers_id == ""){
            alert("不能为空!")
            return false
        }
        // 获取term div
        container = document.getElementById('terminal-container');
        // 生成参数
        //localurl=window.location.href.split('/')[2]
        url = '/pod/terminal/ws' + location.search + '&context=&container=&rows='+rows+'&cols='+cols
        console.log(url)
        // 生成socket对象
        socket = new SockJS(url);
        $("#terminal-container").html("")
        term.open(document.getElementById('terminal-container'));
        term.on('data', function (data) {
            if (socket.readyState == 1) {
                socket.send(data);
            }
        });
        socket.onmessage = function (e) {
            term.write(e.data);
        };
        socket.onclose = function (e) {
             term.write("session is close");
             $("#connect_container").show()
             $("#drop_container").hide()
        };
        socket.onopen = function () {
                resize(socket)
        };
        window.onresize=function(){
                resize(socket)
        }

    }
    function resize(socket) {
        cols=parseInt(document.body.clientWidth /9)
        rows=parseInt(document.documentElement.clientHeight / 20)
        term.resize(cols,rows)
        socket.send('{"cols":'+cols+',"rows":'+rows+'}')
    }

    layui.use(['form', 'table'], function () {
        var $ = layui.jquery;
        var htmls = getQueryString("deploy");
    $('#appNameTitle').html(htmls);


    });
</script>

</body>
</html>

六.完整的控制器和模型代码

6.1.controllers控制器完整代码

控制器部分代码pod.go,放controllers下

// pod.go
package controllers

import (
	//"encoding/json"
	//"fmt"
	"log"
	//"myk8s/common"
	m "myk8s/models"
	"strings"

	beego "github.com/beego/beego/v2/server/web"
	"github.com/tidwall/gjson"
)

type PodController struct {
	beego.Controller
}

func (this *PodController) List() {
	clusterId := this.GetString("clusterId")
	nameSpace := this.GetString("nameSpace")
	deployName := this.GetString("deployName")
	podName := this.GetString("podName")
	nodeName := this.GetString("nodeName")
	labels := this.GetString("labels")
	labelsKV := strings.Split(labels, ":") //yaml里没有对应的appname,deployment不会传递过来
	var labelsKey, labelsValue string
	if len(labelsKV) == 2 {
		labelsKey = labelsKV[0]
		labelsValue = labelsKV[1]
	}
	podList, err := m.PodList(clusterId, nameSpace, deployName, podName, labelsKey, labelsValue, nodeName)
	msg := "success"
	code := 0
	if err != nil {
		log.Println(err)
		code = -1
		msg = err.Error()
	}

	count := len(podList)
	this.Data["json"] = &map[string]interface{}{"code": code, "msg": msg, "count": count, "data": &podList}
	this.ServeJSON()
}

func (this *PodController) ContainerList() {
	clusterId := this.GetString("clusterId")
	nameSpace := this.GetString("nameSpace")
	podName := this.GetString("podName")
	code := 0
	msg := "success"
	xList, err := m.PodContainerList(clusterId, nameSpace, podName)
	if err != nil {
		code = -1
		msg = err.Error()
		log.Printf("[ERROR] ContainerList error:%s\n", err)
	}
	this.Data["json"] = &map[string]interface{}{"code": code, "msg": msg, "count": len(xList), "data": &xList}
	this.ServeJSON()
}

func (this *PodController) Log() {
	clusterId := this.GetString("clusterId")
	nameSpace := this.GetString("nameSpace")
	podName := this.GetString("podName")
	download, _ := this.GetBool("download")
	container := this.GetString("container")
	logLine, _ := this.GetInt64("logLine")

	if this.Ctx.Input.Method() == "POST" {
		gp := gjson.ParseBytes(this.Ctx.Input.RequestBody)
		podName = gp.Get("podName").String()
		logLine = gp.Get("logLine").Int()
		container = gp.Get("container").String()
	}
	if logLine == 0 { //没设置行时,默认为100
		logLine = 100
	}
	if download { //当日志时下载时,不限制行数
		logLine = 0
	}
	log := m.PodLog(clusterId, nameSpace, podName, container, logLine)
	this.Ctx.WriteString(log)
}

6.2.models模型完整代码

podModel.go代码放models下

// podModel.go
package models

import (
	"bytes"
	"context"
	"fmt"

	"io"
	"log"
	"myk8s/common"
	"os"

	//"os"
	"strings"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	//"k8s.io/apimachinery/pkg/runtime"
	//"sigs.k8s.io/yaml"
)

type Podinfo struct {
	PodName   string `json:"podName"` //podname 和podip
	PodIp     string `json:"podIp"`
	NameSpace string `json:"nameSpace"`
	NodeName  string `json:"nodeName"` //节点名称和节点IP
	HostIp    string `json:"hostIp"`
	PodPhase  string `json:"podPhase"` //容器状态 Running
	ImgUrl    string `json:"imgUrl"`
	//PodStatus    PodStatusItem
	RestartCount int32  `json:"restartCount"` //重启次数
	Labels       string `json:"labels"`       //标签
	ResStatus    string `json:"resStatus"`    //cpu及内存使用率
	CreateTime   string `json:"createTime"`
}

type Container struct {
	ContainerName  string `json:"containerName"`
	Envs           string `json:"envs"`
	Mounts         string `json:"mounts"`
	ContainerImage string `json:"containerImage"`
	PullPolicy     string `json:"pullPolicy"`
	Ports          string `json:"ports"`
	ResLimits      string `json:"resLimits"`
	ResRequests    string `json:"resRequests"`
}

func PodList(kubeconfig, namespace, deployName, podName string, labelsKey, labelsValue, nodeName string) ([]Podinfo, error) {

	if namespace == "" {
		//namespace = corev1.NamespaceDefault
		namespace = corev1.NamespaceAll
	}

	clientset := common.ClientSet(kubeconfig)
	var podList *corev1.PodList
	var err error

	//设置ListOptions
	var listOptions = metav1.ListOptions{}
	if labelsKey != "" && labelsValue != "" {
		listOptions = metav1.ListOptions{
			LabelSelector: fmt.Sprintf("%s=%s", labelsKey, labelsValue),
			//FieldSelector: "status.phase=Running,spec.nodeName=ais-master1",
		}
	}

	if nodeName != "" {
		listOptions = metav1.ListOptions{
			FieldSelector: fmt.Sprintf("status.phase!=Succeeded,spec.nodeName=%s", nodeName),
		}
	}

	podList, err = clientset.CoreV1().Pods(namespace).List(context.Background(), listOptions)

	if err != nil {
		log.Printf("list pods error:%v\n", err)
	}
	var bbb = make([]Podinfo, 0)
	for _, pod := range podList.Items {
		//搜索
		if podName != "" {
			if !strings.Contains(pod.Name, podName) {
				//if !strings.HasPrefix(pod.Name,podName)
				continue
			}
		}
		if deployName != "" {
			if !strings.HasPrefix(pod.Name, deployName) {
				continue
			}
		}

		var labelsStr, imgurlStr string
		for kk, vv := range pod.ObjectMeta.Labels {
			labelsStr += fmt.Sprintf("%s:%s,", kk, vv)
		}
		if len(labelsStr) > 0 {
			labelsStr = labelsStr[0 : len(labelsStr)-1]
		}

		var containerState = fmt.Sprintf("%v", pod.Status.Phase)

		var restartNum int32
		if len(pod.Status.ContainerStatuses) > 0 {
			imgurlStr = pod.Status.ContainerStatuses[0].Image
			restartNum = pod.Status.ContainerStatuses[0].RestartCount
			if containerState == "Pending" {
				containerState = pod.Status.ContainerStatuses[0].State.Waiting.Reason
			}
		}

		Items := &Podinfo{
			PodName:      pod.Name,
			PodIp:        pod.Status.PodIP,
			ImgUrl:       imgurlStr,
			NameSpace:    pod.ObjectMeta.Namespace,
			NodeName:     pod.Spec.NodeName,
			HostIp:       pod.Status.HostIP,
			PodPhase:     containerState,
			Labels:       labelsStr,
			RestartCount: restartNum,
			CreateTime:   pod.ObjectMeta.CreationTimestamp.Format("2006-01-02 15:04:05"),
		}
		bbb = append(bbb, *Items)
	}
	return bbb, err
}

func PodContainerList(kubeconfig, nameSpace, podName string) ([]Container, error) {
	var ccc = make([]Container, 0)
	pod, err := common.ClientSet(kubeconfig).CoreV1().Pods(nameSpace).Get(context.TODO(), podName, metav1.GetOptions{})
	if err != nil {
		return ccc, err
	}
	for _, v1 := range pod.Spec.Containers {
		xItems := &Container{
			ContainerName:  v1.Name,
			Envs:           "",
			Mounts:         "",
			ContainerImage: v1.Image,
			PullPolicy:     fmt.Sprintf("%v", v1.ImagePullPolicy),
			Ports:          "",
			ResLimits:      fmt.Sprintf("CPU:%s,Memory:%s", v1.Resources.Limits.Cpu().String(), v1.Resources.Limits.Memory().String()),
			ResRequests:    fmt.Sprintf("CPU:%s,Memory:%s", v1.Resources.Requests.Cpu().String(), v1.Resources.Requests.Memory().String()),
		}
		ccc = append(ccc, *xItems)
	}
	return ccc, nil
}

func PodLog(kubeconfig, nameSpace, podName, container string, logLine int64) string {
	clientset := common.ClientSet(kubeconfig)
	pod, err := clientset.CoreV1().Pods(nameSpace).Get(context.TODO(), podName, metav1.GetOptions{})
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get pod %q: %v\n", podName, err)
		//os.Exit(1)
	}
	// 获取指定行数日志
	//logs, err := clientset.CoreV1().Pods(nameSpace).GetLogs(podName, &corev1.PodLogOptions{TailLines: &lines})
	//获取实时日志
	var logOptions = &corev1.PodLogOptions{}
	logOptions.Follow = false     //持续输出
	logOptions.Timestamps = false //显示时间戳
	//var line int64 = 50
	if logLine > 0 {
		//line := logLine
		logOptions.TailLines = &logLine //获取多少行日志 需要指针类型https://blog.csdn.net/weixin_36469852/article/details/127432931
	}

	var containerPt *string = &container
	if container == "" {
		// [CONTAINER] (container as arg not flag) is supported as legacy behavior. See PR #10519 for more details.
		if len(pod.Spec.Containers) != 1 {
			podContainersNames := []string{}
			for _, container := range pod.Spec.Containers {
				podContainersNames = append(podContainersNames, container.Name)
			}
		} else {
			containerPt = &pod.Spec.Containers[0].Name
		}
	}
	logOptions.Container = *containerPt //指定container来获取日志,需要指针类型https://blog.csdn.net/weixin_36469852/article/details/127432931

	podLogs, err := clientset.CoreV1().Pods(nameSpace).GetLogs(podName, logOptions).Stream(context.TODO())
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get logs for pod %q: %v\n", podName, err)
		//os.Exit(1)
		return "error"
	}
	defer podLogs.Close()

	//单次输出
	buf := new(bytes.Buffer)
	_, err = io.Copy(buf, podLogs)
	if err != nil {
		return "error copy"
	}
	return buf.String()

	//流式输出日志内容
	// buf := make([]byte, 1024)
	// for {
	// 	n, err := podLogs.Read(buf)
	// 	if err != nil && err == io.EOF {
	// 		break
	// 	}
	// 	fmt.Print(string(buf[0:n]))
	// }
	// return "test"
}

七.效果如下图

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述