優秀的系統都是根據反饋逐漸完善出來的
上篇文章介紹了我們為了應對安全和多分支頻繁測試的問題而開發了一套Alodi系統,Alodi可以通過一個按鈕快速構建一套測試環境,生成一個臨時訪問地址,詳細信息可以看這一篇文章:Alodi:為了保密我開發了一個系統
系統上線后,SSH登陸控制台成了一個迫切的需求,Kubernetes的Dashboard控制台雖然有WebSSH的功能,但卻沒辦法跟Alodi系統相結合,決定在Alodi中集成WebSSH的功能,先來看看最后實現的效果吧
涉及技術
- Kubernetes Stream:接收數據執行,提供實時返回數據流
- Django Channels:維持長連接,接收前端數據轉給Kubernetes,同時將Kubernetes返回的數據發送給前端
- xterm.js:一個前端終端組件,用於模擬Terminal的界面顯示
基本的數據流向是:用戶 --> xterm.js --> django channels --> kubernetes stream,接下來看看具體的代碼實現
Kubernetes Stream
Kubernetes本身提供了stream方法來實現exec的功能,返回的就是一個WebSocket可以使用的數據流,使用起來也非常方便,代碼如下:
from kubernetes import client, config
from kubernetes.stream import stream
class KubeApi:
def __init__(self, namespace='alodi'):
config.load_kube_config("/ops/coffee/kubeconfig.yaml")
self.namespace = namespace
def pod_exec(self, pod, container=""):
api_instance = client.CoreV1Api()
exec_command = [
"/bin/sh",
"-c",
'TERM=xterm-256color; export TERM; [ -x /bin/bash ] '
'&& ([ -x /usr/bin/script ] '
'&& /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) '
'|| exec /bin/sh']
cont_stream = stream(api_instance.connect_get_namespaced_pod_exec,
name=pod,
namespace=self.namespace,
container=container,
command=exec_command,
stderr=True, stdin=True,
stdout=True, tty=True,
_preload_content=False
)
return cont_stream
這里的pod name可以通過list_namespaced_pod
方法獲取,代碼如下:
def get_deployment_pod(self, RAND):
api_instance = client.CoreV1Api()
try:
r = api_instance.list_namespaced_pod(
namespace=self.namespace,
label_selector="app=%s" % RAND
)
return True, r
except Exception as e:
return False, 'Get Deployment: ' + str(e)
state, data = self.get_deployment_pod(RAND)
pod_name = data.items[0].metadata.name
list_namespaced_pod
會列出namespace下所有pod的詳細信息,這里傳了兩個參數,第一個namespace
是必須的,表示我們要列出pod的namespace,第二個label_selector
非必須,表示可以通過設置的標簽過濾namespace下的pod,由於我們在創建的時候給每個deployment都添加了唯一的app=RAND
的標簽,所以這里可以過濾出來我們項目所對應的pod
一個deployment可能對應多個pod,獲取到的data.items
包含了所有的pod信息,為一個list列表,可根據需要取到對應pod的name
Django Channels
之前有兩篇文章詳細介紹過Django Channels,不了解的可以先查看:Django使用Channels實現WebSocket--上篇和Django使用Channels實現WebSocket--下篇,最重要的兩部分代碼如下
routing代碼:
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path, re_path
from medivh.consumers import SSHConsumer
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter([
re_path(r'^pod/(?P<name>\w+)', SSHConsumer),
])
),
})
正則匹配所有以pod
開頭的websocket連接,都交由名為SSHConsumer
的Consumer處理,Consumer代碼如下:
from channels.generic.websocket import WebsocketConsumer
from medivh.backends.kube import KubeApi
from threading import Thread
class K8SStreamThread(Thread):
def __init__(self, websocket, container_stream):
Thread.__init__(self)
self.websocket = websocket
self.stream = container_stream
def run(self):
while self.stream.is_open():
if self.stream.peek_stdout():
stdout = self.stream.read_stdout()
self.websocket.send(stdout)
if self.stream.peek_stderr():
stderr = self.stream.read_stderr()
self.websocket.send(stderr)
else:
self.websocket.close()
class SSHConsumer(WebsocketConsumer):
def connect(self):
self.name = self.scope["url_route"]["kwargs"]["name"]
# kube exec
self.stream = KubeApi().pod_exec(self.name)
kub_stream = K8SStreamThread(self, self.stream)
kub_stream.start()
self.accept()
def disconnect(self, close_code):
self.stream.write_stdin('exit\r')
def receive(self, text_data):
self.stream.write_stdin(text_data)
WebSSH可以看作是一個最簡單的websocket長連接,每個連接建立后都是獨立的,不會跟其他連接共享數據,所以這里不需要用到Group
當連接建立時通過self.scope
獲取到url中的name,傳給Kubernetes API,同時會新起一個線程不斷循環是否有新數據產生,如果有則發送給websocket
當websocket接收到數據就直接寫入Kubernetes API,當websocket關閉則會發送個exit
命令給Kubernetes
前端頁面
前端主要用到了xterm.js,整體代碼也比較簡單
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Alodi | Pod Web SSH</title>
<link rel="Shortcut Icon" href="/static/img/favicon.ico">
<link href="/static/plugins/xterm/xterm.css" rel="stylesheet" type="text/css"/>
<link href="/static/plugins/xterm/addons/fullscreen/fullscreen.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="terminal"></div>
</body>
<script src="/static/plugins/xterm/xterm.js"></script>
<script src="/static/plugins/xterm/addons/fullscreen/fullscreen.js"></script>
<script>
var term = new Terminal({cursorBlink: true});
term.open(document.getElementById('terminal'));
// xterm fullscreen config
Terminal.applyAddon(fullscreen);
term.toggleFullScreen(true);
var socket = new WebSocket(
'ws://' + window.location.host + '/pod/{{ name }}');
socket.onopen = function () {
term.on('data', function (data) {
socket.send(data);
});
socket.onerror = function (event) {
console.log('error:' + e);
};
socket.onmessage = function (event) {
term.write(event.data);
};
socket.onclose = function (event) {
term.write('\n\r\x1B[1;3;31msocket is already closed.\x1B[0m');
// term.destroy();
};
};
</script>
</html>
term.open
初始化一個Terminal
term.on
會將輸入的內容全部實時的傳遞給后端
xterm.js有一個fullscreen的插件,引入之后可以配置fullscreen,否則可能頁面只有一部分terminal窗口
目前仍然遇到一個窗口大小無法調整的問題沒有解決,初步判斷是后端Kubernetes傳回的數據決定的,查詢了相關資料,找到kubectl
命令可以通過添加COLUMNS
和LINES
的env來設置
#!/bin/sh
if [ "$1" = "" ]; then
echo "Usage: kshell <pod>"
exit 1
fi
COLUMNS=`tput cols`
LINES=`tput lines`
TERM=xterm
kubectl exec -i -t $1 env COLUMNS=$COLUMNS LINES=$LINES TERM=$TERM bash
但Kubernetes Python API的Stream沒有找到配置的地方,如果你知道,麻煩告訴我
相關文章推薦閱讀: