世界上最简单的Kubernetes仪表板:k1s


我用50多行Bash代码实现了我称之为“世界上最简单的Kubernetes仪表板”,它被称为k1s,本文将介绍如何使用它以及它是如何工作的。


当然,“世界上最简单”并不是一个很严格的说辞。
你可以在GitHub上的仓库weibeld/k1s上找到完整代码和详细使用说明。

概述

k1s大致如下图:
1.gif

仪表板展示了任意命名空间(或跨所有命名空间)中任何类型的资源列表,并实时更新它。某些类型的资源会显示额外的信息,例如Pod的当前状态,Deployment中所需的副本数和实际数量。

你可以运行多个k1s实例从而可以同时观察多个资源类型的更新。

下图是运行三个仪表板实例的例子,一个用于Deployment,一个用户ReplicasSet,还有一个用于Pod(使用tmux运行)。然后对Deployment上执行一些伸缩操作和滚动更新。
2-min.gif

请注意,你可以实时观察Deployment、其管理的ReplicaSets和Pods之间的交互。这有可能提供许多认知,有助于了解Deployment和其他资源的工作原理。

安装

如果你在macOS上使用homebrew,你可以这样安装:
$ brew install weibeld/core/k1s

如果是其他情况,你可以参考下面脚本安装它:
{
wget https://raw.githubusercontent.com/weibeld/k1s/master/k1s
chmod +x k1s
mv k1s /usr/local/bin


k1s依赖于机器上安装的以下工具:


可能你已经拥有大部分工具,要是没有的话你可以在GitHub仓库上找到安装向导。

使用

k1s是一个可直接在你本机运行的Bash脚本,它的命令行接口如下:
$ k1s [namespace] [resource-type]

两个额外的命令行参数分别是:
  • namespace:指定要观察的资源所在的Kubernetes命名空间(默认是default
  • resource-type:指定要观察的Kubernetes资源类型(默认是pods


如果需要跨所有命名空间观察资源,你可以指定命名空间参数为-

资源类型参数的值可以指定为Kubernetes接受的任意名称,包括复数模式,单数模式和简写(如果可用的话)。例如以下参数都是有效的:
  • deployments、deployment、deploy
  • replicasets、replicaset、rs
  • services、service、svc


k1s是由本机上的kubeconfig文件支撑的。这意味着k1s始终是连接到你的kubeconfig文件中指定的集群。换句话说,k1s默认使用与kubectl使用的同一个集群。

如要退出仪表板请键入Ctrl-C。

实现

现在,我们来谈谈k1s的实现,上面提到k1s是一个Bash脚本,k1s v0.1.0版本的完整代码:
#!/bin/bash

[[ "$1" = -v || "$1" = --version ]] && { echo "0.1.1"; exit; }
for d in jq watch curl kubectl; do which "$d" >/dev/null || { echo "Missing dependency: $d"; exit 1; }; done
ns=${1:-default}; res=${2:-pods}

c() { echo -e "\033[$1m"; }
cc() { echo -e "\033[$1;1m"; }

printf Loading && while true; do printf . && sleep 0.1; done &

set -o pipefail
path=$(kubectl get "$res" "$([[ "$ns" = - ]] && echo --all-namespaces || echo -n=$ns)" -v 6 2>&1 >/dev/null | grep GET | tail -n 1 | sed -n 's#.*https://[^/]*\([a-z0-9/.-]*\).*#\1#p')
pid=$?
kill -9 "$!" && wait "$!" 2>/dev/null
[[ "$pid" -ne 0 ]] && echo -e "\nInvalid resource type: $res" && exit 1
[[ $(echo -n "${path//[^\/]}" | wc -c) -lt 5 ]] && ns=-
res=${path##*/}

exec 3< <(kubectl proxy -p 0)
port=$(head -n 1 <&3 | sed 's/.*:\([0-9]\{4,5\}\)\b.*/\1/')

file=$(mktemp)
cat <<EOF >"$file"
$(cc 36) ____ ____ ____
||$(cc 33)k$(cc 36) |||$(cc 33)1$(cc 36) |||$(cc 33)s$(cc 36) ||  $(cc 0)Kubernetes Dashboard$(cc 36)
||__|||__|||__||  $(cc 0)Namespace: $ns$(cc 36)
|/__\|/__\|/__\|  $(cc 0)Resources: $res$(c 0)
EOF

curl -N -s "http://localhost:$port$path?watch=true" |
while read -r event; do
name=$(jq -r '.object.metadata.name' <<<"$event")
case "$res" in
pods)
  phase=$(jq -r '.object.status.phase' <<<"$event")
  is_ready=$(jq -r 'if .object.status | has("conditions") then .object.status.conditions[] | if select(.type=="Ready").status=="True" then "1" else "" end else "" end' <<<"$event")
  is_scheduled=$(jq -r 'if .object.status | has("conditions") then .object.status.conditions[] | if select(.type=="PodScheduled").status=="True" then "1" else "" end else "" end' <<<"$event")
  [[ "$is_scheduled" && ! "$is_ready" ]] && info=NonReady || info=$phase
  [[ "$info" = Running ]] && info=$(c 32)$info$(c 0) || info=$(c 33)$info$(c 0) ;;
deployments|replicasets|statefulsets)
  spec=$(jq -r '.object.spec.replicas' <<<"$event")
  stat=$(jq -r '.object.status.readyReplicas // 0' <<<"$event")
  [[ "$stat" = "$spec" ]] && info="$(c 32)($stat/$spec)$(c 0)" || info="$(c 33)($stat/$spec)$(c 0)" ;;
esac
case $(jq -r .type <<<"$event") in
  ADDED) echo "$name $info" >>"$file" ;;
  MODIFIED) sed -i "s/^$name .*$/$name ${info//\//\\/}/" "$file" ;;
  DELETED) sed -i "/^$name .*$/d" "$file";;
esac
done &

watch -ctn 0.1 cat "$file"

下面我们将一步步阅读项目代码。不过首先我们先来讨论一下k1s所基于的思路。

基础机制

以下是k1s工作的主要部分:
  1. 使用kubectl proxy用于轻松请求Kubernetes API服务(第20行)。
  2. 使用curl执行一个向Kubernetes API服务请求指定资源类型的watch监视请求(第32行)。这将向API服务器创建一个持久连接,该连接返回任意ADDEDMODIFIEDDELETED类型的JSON格式资源事件流。
  3. 使用jq解析每个事件并从中提取信息,例如事件相关的资源的名称。将该信息作为额外的记录行写入到文件系统上的一个文件(称之为状态文件)。如果资源在文件中已存在,则更新它,如果资源被删除,则从状态文件中删除具体行(第48-50行)。
  4. 使用watch定期更新显示该状态文件(第53行)。这给人一种动态全屏终端应用的印象。


简而言之,k1s会不断地将某个资源类型的状态整合到一个文件中,并将这个文件用watch显示出来,这样你就可以实时观察到任何更新。

现在我们来看看实际代码。

具体实现

k1s脚本从以下代码行开始:
3.png

这将执行一些基本检查和初始化:
  • 第三行提供了-v/--version选项用于打印k1s的版本。
  • 第四行检查所有依赖是否已在本机安装,如果没有则显示错误信息并退出。
  • 第五行将命令行参数读进变量(namespace和resource-type),如果这些参数没有提供则使用默认值。


接下来的两行代码如下:
4.png

这定义了两个Bash函数用于输出ANSI转义序列,用来以特定的颜色打印文本。这些函数将在以下用来给k1s的输出着色。

接下来是这行:
5.png

此行打印启动k1s后立即显示的加载指示器。此任务由一个每0.1秒打印字符.的无限循环组成,并作为后台进程启动。当k1s初始化完成之后该进程将会被杀死。

接着是下一段代码:
6.png

此段代码实现了k1s的主要初始化步骤。它确定了用户请求的资源类型和命名空间的Kubernetes API路径:
  • 第12行启用Bash的pipefail选项,如果管道语句中包含的任何一条命令返回一个非零的退出码,那么该选项就会使整个管道语句返回一个非零的退出码。这是后续命令正常执行的前提。
  • 第13行执行了请求的资源类型和命令空间的kubectl get请求并从输出中提取相应的Kubernetes API路径。这里的诀窍是通过提高kubectl的输出级别(-v=6),kubectl会输出它向Kubernetes API服务器发出的HTTP GET请求的确切URL。通过这个URL,可通过grepsed提取出来。
  • 第14行记录了13行中kubectl get的退出码,如果请求成功则退出码为零,否则为非零。退出码非零的很大可能是用户提交了一个无效的资源类型(例如使用dep替代deploy/deployment)。
  • 第15行杀掉第10行开启的加载指示器后台进程。注意加载指示器的进程ID(PID)被指定为Bash内置变量$!。在Bash中$!变量始终保存已启动的最后一个后台进程的进程ID。因为从第10行后没有其他后台进程启动,所以该变量保证是指向加载指示器进程。
  • 第16行检查第13行kubectl get请求的退出码,如果退出码非零,则假设用户提供了一个无效的资源类型规范,之后脚本以相应的错误信息终止。


接下来的两行具有相当的装饰用途:
  • 第17行检查提取的API路径是否包含了命名空间部分。如果没有,则代表着用户请求跨所有命名空间的资源类型或者是非命名空间级别的资源类型。在这种情况下,命名空间将会被设置并在仪表板中显示为为-。这修复了当用户指定了非命名空间级别资源却同时指定命名空间的情况。
  • 第18行将从API路径中提取的用户请求的资源类型设置为相应资源的复数形式,该值会显示在仪表板的标题上。这意味着如果用户指定deploy/deployment来监视Deployments,那么仪表板会在标题横幅处统一显示为deployments


紧接着是下面两行代码:
7.png

这两行脚本配置了kubectl proxy并在之后被用于连接Kubernetes API服务:
  • 第20行启动kubectl proxy作为后台进程,该命令为Kubernetes API服务创建一个本地代理,并在本机监听一个随机端口(通过-p=0指定)。该后台进程的输出被新文件描述符3捕获。
  • 第21行从上一行的kubectl proxy的输出文件描述符中提取端口。知道这个端口号在后续代码中才能构建Kubernetes API服务的监视请求。



在这个StackOverflow的回答中,介绍了上述捕捉后台进程输出的技术。
接着是下一段代码:
8.png

这段代码创建并初始化状态文件,该文件之后会被watch命令调用显示:
  • 第23行创建状态文件
  • 第24-30行将标题横幅写入到状态文件。 标题横幅由(彩色的)k1s logo和其他一些信息组成,包括正在使用的命名空间和资源类型。


下一段代码如下:
9.png

这段代码执行向Kubernetes API服务发起的实际监视请求并处理事件流响应。以上代码实际上是单个Bash语句,其将curl命令输出到管道并通过while循环从管道中读取curl命令的输出:
  • 第32行使用curl发起监视请求。URL由之前确定的Kubernetes API路径和kubectl proxy端口号组成。监视请求返回的每个响应都是对应于一个资源事件的JSON单行。curl使用了-N选项禁止输出缓冲,这样每一行在收到后就会立即输出。
  • 第33行开启一个while循环读取curl的每一行输出到名为event的变量。该循环会无限运行,只有当用户键入Ctrl-C退出仪表板时才被终止。


循环体从34行开始到51行。开头的几行从JSON事件对象中提取一些数据,传递到循环的主体中:
  • 第34行提取事件相关的资源的名称(即创建、修改或删除的资源),提取操作使用jq完成。
  • 第35行使用case语句根据正在监视的资源类型执行不同的操作。其目的是对某些资源类型进行特殊处理,并确定和提取它们的附加信息。


第36到41行以特别的方式处理Pods:
  • 第37行提取Pod的当前阶段。阶段是对Pod状态的高级指示,如Running或Pending。
  • 第38行确定Pod的Ready条件是否设置为True。如果是的话,那么Pod中的所有容器都通过了就绪探测,Pod可以为用户请求提供服务。
  • 第39行确定Pod的PodScheduled条件是否设置为True。如果是的话,那么Pod已被Kubernetes调度器分配了一个节点。
  • 第40行确定k1s将显示的Pod的状态。默认情况下,k1s显示Pod的阶段。但是,在下面的情况下,行为会有所不同:如果Pod已经被调度,但目前还没有准备好,那么k1s显示NonReady而不是phase字段(此时phasePending)。这使得它更容易区分尚未调度的Pod和仅仅是临时暂停或终止的Pod。
  • 第41行为Pod的状态指示器着色,以获得更好的视觉效果:如果状态为Running则为绿色,其他情况为黄色。


第42到45行以特别的方式处理Deployments、ReplicaSets和StatefulSets(这些都是Pod控制器)。
  • 第43行确定Pod控制器指定的(需要的)副本的数量
  • 第44行确定Pod控制器当前可用(准备好)的副本数量。
  • 第45行格式化并着色副本指示器:如果可用的副本数量符合要求,则为绿色,否则为黄色。


接下来,第47行到51行根据事件的类型和上面确定的信息更新状态文件。
10.png

  • 第47行启动一个case语句根据事件的类型采取不同的操作,事件类型可以是ADDEDMODIFIED或是DELETED
  • 第48行处理ADDED事件:如果添加了一个资源,它的名称(以及之前确定的任何附加的资源类型特定信息)将被简单地追加为一个新行到状态文件中。
  • 第49行处理MODIFIED事件:如果一个资源被修改(如Pod的相位改变),状态文件中该资源的现有行将被一个由新信息组成的新行取代。
  • 第50行处理DELETED事件:如果资源被删除,状态文件中相应行也会被删除。


最后只剩下一行了:
11.png

此行执行watch命令来定时更新并显示状态文件在你的终端窗口。更新时间间隔设置为0.1秒,这个时间间隔小到足以让人眼看似 "实时"。

k1s的完整代码总共就只有这么多!

讨论和结论

正如其口号所说,k1s的重点是简单和简洁。这就带来了一些局限性,必须指出:
  • 可能目前最大的限制是,当仪表盘中的项目列表超过终端窗口的高度时,窗口底部以外的所有输出都会被截断,没有办法滚动。这是因为watch不允许在超过终端高度的文件中滚动。如果你知道什么简单的方法来处理这个问题,请在评论中告诉我。
  • 如果能显示更多的附加信息,包括其他资源类型的信息(如服务和端点),将会很有意思。然而,这有可能是一个无底洞,因为总是有更多的东西可以显示,而且不同的人会关注于不同的东西。然而,由于目前的代码是如此简短简单,所以应该很容易为特定的用例或调查来按需定制。
  • 该工具依赖于其他工具的输出格式,尤其是kubectl。这意味着,如果这些工具的输出格式发生变化,那么k1s也需要进行调整。不过考虑到k1s代码的简洁性,这应该是一个相对快速的任务。


当然,大多数这些限制可以通过用真正的编程语言(比如Go)实现k1s来解决,就像所有成熟的Kubernetes仪表盘一样。然而,k1s一开始也是作为实验性的,用尽可能简单的手段(和尽可能少的代码)来实现一些有用的东西。实际上,它一开始是一个Bash单行脚本,用于显示默认命名空间中的当前Pod的集合。

考虑到这一重点,该工具仍然被证明是令人惊讶的有用的(至少对于实验或教育用例),它可以作为围绕Kubernetes的高级脚本的基础。

本着这种精神,请尽情使用k1s,并提交问题PR

原文链接:The world’s simplest Kubernetes dashboard: k1s(翻译:冯旭松)

1 个评论

原文还在持续更新中。。。

要回复文章请先登录注册