kubectl completion zshを車輪の再発明した

nnao45
24 min readDec 4, 2018

--

https://github.com/nnao45/zsh-kubectl-completion

まぁこういう物を作ったって話です。

まず初めに

この記事はKubernetesアドベントカレンダー(https://qiita.com/advent-calendar/2018/kubernetes)の5日目の記事です。

Kubernetesをオペレーションしていくと真っ先に思うのが「kubectlをまともに打っていくという行為が辛み」という事です。例えば、「production namespaceのnginxってserviceをdescribeコマンドで詳細を見たい」となると、以下のコマンドを打たないといけません。

$ kubectl --namespace production describe service nginx

なげえ。

いや長いどころか、色々なことを思い出しながらやらなければなりません。

  • productionってnamespaceってあるのか?
  • nginxってserviceって名前nginx?nginx-svcじゃ無く?
  • describeのスペルってこれで合ってる?

などをkubectlに怒られながら打っていくわけです。やってられません。しかもlogsコマンドなんかだとまた記法がちょっと違って、

$ kubectl --namespace staging logs deployments/qicoo-api

これもまたこんなことを思い出しながらやらないといけないはずです。

  • stagingってnamespaceってあるのか?
  • logsコマンド?logコマンドじゃなかったっけ?
  • logsコマンドってpod以外に使う場合はdeployments/qicoo-api?deployments qicoo-api?

覚えてられません。。。毎度 kubectl help を打つ毎日を過ごす日々がしばらく続き。。。これは辛いと思いGoogle先生に聞くわけです。

そうだ、補完しよう。

そうなるわけです。幸いにもkubectlには元来、 bashzsh に関しては補完コマンドを用意してくれています。

$ kubectl completion <zsh/bash>

これで zshbash の補完関数がでてくるので、それぞれのシェルに応じて設定してあげればめでたしめでたしですね。

例えば zsh の場合は@mollifier氏の zload 関数を使えば気軽に試せますので、参考にしてください(zsh で補完関数を作るときに便利な再読み込みするプラグインを作った)。

$ kubectl completion zsh > ./_kubectl
$ zload _kubectl

さて、これで満足、、、カモなのですが、、、ここで貴方が zsh を使って数ヶ月は経つのであれば、とある疑問に合うはずなのです。そう、それこそが今回の主題なのです

おことわり:ここからzshの話しかしません。とりあえずもういいやという方でも、使ってみてよさそうであればgithubのページにスターいただければ開発の励みになります。

あれ、このzshの補完って・・・・?

zsherでご聡明な皆さんはすぐにお気づきでしょう、 kubectl completion zshzshの拡張補完でない事を。

これはどういう事なんだろうと思って、以下を見てみると分かります、、、、

$ kubectl completion zsh > _kubectl_zsh
$ kubectl completion bash > _kubectl_bash
$ diff -u _kubectl_bash _kubectl_zsh
--- _kubectl_bash 2018-12-02 12:59:02.000000000 +0900
+++ _kubectl_zsh 2018-12-02 12:58:56.000000000 +0900
@@ -1,3 +1,4 @@
+#compdef kubectl
# Copyright 2016 The Kubernetes Authors.
#
@@ -12,6 +13,147 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+
+__kubectl_bash_source() {
+ alias shopt=':'
+ alias _expand=_bash_expand
+ alias _complete=_bash_comp
+ emulate -L sh
+ setopt kshglob noshglob braceexpand
+
+ source "$@"
+}
+
+__kubectl_type() {
+ # -t is not supported by zsh
+ if [ "$1" == "-t" ]; then
+ shift
+
+ # fake Bash 4 to disable "complete -o nospace". Instead
+ # "compopt +-o nospace" is used in the code to toggle trailing
+ # spaces. We don't support that, but leave trailing spaces on
+ # all the time
+ if [ "$1" = "__kubectl_compopt" ]; then
+ echo builtin
+ return 0
+ fi
+ fi
+ type "$@"
+}
+
+__kubectl_compgen() {
+ local completions w
+ completions=( $(compgen "$@") ) || return $?
+
+ # filter by given word as prefix
+ while [[ "$1" = -* && "$1" != -- ]]; do
+ shift
+ shift
+ done
+ if [[ "$1" == -- ]]; then
+ shift
+ fi
+ for w in "${completions[@]}"; do
+ if [[ "${w}" = "$1"* ]]; then
+ echo "${w}"
+ fi
+ done
+}
+
+__kubectl_compopt() {
+ true # don't do anything. Not supported by bashcompinit in zsh
+}
+
+__kubectl_ltrim_colon_completions()
+{
+ if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
+ # Remove colon-word prefix from COMPREPLY items
+ local colon_word=${1%${1##*:}}
+ local i=${#COMPREPLY[*]}
+ while [[ $((--i)) -ge 0 ]]; do
+ COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
+ done
+ fi
+}
+
+__kubectl_get_comp_words_by_ref() {
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[${COMP_CWORD}-1]}"
+ words=("${COMP_WORDS[@]}")
+ cword=("${COMP_CWORD[@]}")
+}
+
+__kubectl_filedir() {
+ local RET OLD_IFS w qw
+
+ __kubectl_debug "_filedir $@ cur=$cur"
+ if [[ "$1" = \~* ]]; then
+ # somehow does not work. Maybe, zsh does not call this at all
+ eval echo "$1"
+ return 0
+ fi
+
+ OLD_IFS="$IFS"
+ IFS=$'\n'
+ if [ "$1" = "-d" ]; then
+ shift
+ RET=( $(compgen -d) )
+ else
+ RET=( $(compgen -f) )
+ fi
+ IFS="$OLD_IFS"
+
+ IFS="," __kubectl_debug "RET=${RET[@]} len=${#RET[@]}"
+
+ for w in ${RET[@]}; do
+ if [[ ! "${w}" = "${cur}"* ]]; then
+ continue
+ fi
+ if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
+ qw="$(__kubectl_quote "${w}")"
+ if [ -d "${w}" ]; then
+ COMPREPLY+=("${qw}/")
+ else
+ COMPREPLY+=("${qw}")
+ fi
+ fi
+ done
+}
+
+__kubectl_quote() {
+ if [[ $1 == \'* || $1 == \"* ]]; then
+ # Leave out first character
+ printf %q "${1:1}"
+ else
+ printf %q "$1"
+ fi
+}
+
+autoload -U +X bashcompinit && bashcompinit
+
+# use word boundary patterns for BSD or GNU sed
+LWORD='[[:<:]]'
+RWORD='[[:>:]]'
+if sed --help 2>&1 | grep -q GNU; then
+ LWORD='\<'
+ RWORD='\>'
+fi
+
+__kubectl_convert_bash_to_zsh() {
+ sed \
+ -e 's/declare -F/whence -w/' \
+ -e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
+ -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
+ -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
+ -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
+ -e "s/${LWORD}_filedir${RWORD}/__kubectl_filedir/g" \
+ -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__kubectl_get_comp_words_by_ref/g" \
+ -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__kubectl_ltrim_colon_completions/g" \
+ -e "s/${LWORD}compgen${RWORD}/__kubectl_compgen/g" \
+ -e "s/${LWORD}compopt${RWORD}/__kubectl_compopt/g" \
+ -e "s/${LWORD}declare${RWORD}/builtin declare/g" \
+ -e "s/\\\$(type${RWORD}/\$(__kubectl_type/g" \
+ <<'BASH_COMPLETION_EOF'
# bash completion for kubectl -*- shell-script -*-
__kubectl_debug()
@@ -7720,3 +7862,9 @@
fi
# ex: ts=4 sw=4 et filetype=sh
+
+BASH_COMPLETION_EOF
+}
+
+__kubectl_bash_source <(__kubectl_convert_bash_to_zsh)
+_complete kubectl 2>/dev/null

こ、これだけしか差分がない・・・・だと・・・・?

そう、、、、これは bash をベースにして移植した補完関数なんだということに気づきます。決定的なのが、 zsh の方の補完が、

__start_kubectl()
{
local cur prev words cword
declare -A flaghash 2>/dev/null || :
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -s || return
else
__kubectl_init_completion -n "=" || return
fi
local c=0
local flags=()
local two_word_flags=()
local local_nonpersistent_flags=()
local flags_with_completion=()
local flags_completion=()
local commands=("kubectl")
local must_have_one_flag=()
local must_have_one_noun=()
local last_command
local nouns=()
__kubectl_handle_word
}
if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_kubectl kubectl
else
complete -o default -o nospace -F __start_kubectl kubectl
fi

なんだと・・・!? complete コマンドで補完に入れてるのか・・・・!?

completeコマンドはbashのビルトイン関数であり、これが告げる決定的なことは、この補完関数からネイティブにzshの拡張補完にアクセスできないということです。

completeのman https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#Programmable-Completion-Builtins

えぬなお自身、「まぁ実際何個か修正すればzshのフラグ補完もできるようになるだろ」とか思ったが、なにせcompleteコマンドを関数指定で補完しているのである。これが何がキツイかというと、そもそも zsh で補完関数を設計する場合は明示的に compadd などで補完追加したりと、補完するに至る経路が違うのだ。まぁぶっちゃけソースコード読むのだるすぎて修正しきるより1から作った方が早いだろって感じだった。

ということで作った。

  • kubernetes v1.12.2対応
  • 全コマンドとその配下のオプションの補完
  • 本家にある便利機能の以下のフラグをkubectlの後につけることで、その後の補完されるリソースをフィルタする機能も完備
  • --kubeconfig
  • --cluster
  • --user
  • --context
  • --namespace
  • --server
  • 全オプションの説明をつけた ←new!!
  • 全サブコマンドにリソース情報もつけて分かりやすく ←new!!
  • zplugやpreztoでプラグイン管理しやすいフォーマットに ←new!!

実装はまだ甘いところが目立つが、まぁ十分動かせる代物にはなったと思いたい。

本体である_kubectl見てもらうと一気にブラウザが重くなるのでお察しなのですが、zshの補完関数はシェルの縛りで1枚のファイルにまとめる必要があるので、1400行弱のラッパーとパーサーの塊みたいなソースファイルになりましたよと(ちなみにdockerの公式zsh補完は3000行超えなので、まぁ雰囲気それくらいのボリュームになるのは納得されるかなと・・・)。

という感じで、このオプションやコマンドの意味ってなんだっけという時に調べなくてもよくなるわけである。あとリソースを調べるときは kubectl get を打って、、、じゃなくて補完を打てば概要は付いてくるので便利(だと思って自分は使っているw)

一応バージョンアップと一緒にメンテして行こうと思っている(これを書いてる途中に早速v1.13.0が出たようで、まぁ頑張って実装してないサブコマンドも含めて対応しようと思う、ああ後この記事の英語版もな。。。)。

どうやってインスコするの?

試すだけなら、↑の方にある zload と同じ要領で追加してOK。

インスコしたい場合は、zplugだと、

zplug "nnao45/zsh-kubectl-completion"

とzshrcに書いて終わりだ。

もしマニュアルで入れたい場合は、 fpath~/.zsh/completion_kubectlを追加して補完関数を作って入れればいい。

$ curl -fLo ~/.zsh/completion_kubectl \ 
https://raw.githubusercontent.com/nnao45/zsh-kubectl-completion/master/_kubectl

まぁこの辺とかを見てくれ。

一応技術の話を・・・(需要あるのか?)

迷ったが、一応知りたいごく一部の方に関数の流れを書いておく。zshのデザインパターンとしてはそれほど奇抜なことはしていない王道だと思う。

zshの補完関数は compdef コマンドで定義を行う。↑のレポジトリの一行目にある、

#compdef kubectl 

補完用関数は _ アンダースコアから始める決まりとなっているので、関数ファイルとして作成する場合 _kubectl として作りfpathというzshの補完関数をおくべきパスにおいておくとと自動的にzshの補完関数として立ち上げ時に読み込まれる。

zshは様々な補完関数がビルトインで用意されているが、感覚的には例えば一番単純な補完追加関数の compadd とかを使用した場合は、

$ compadd apply create 

などとやれば先ほと compdef で定義したコマンドに対して補完が追加される。これくらいならシンプルな話だ。

今回は _argument 関数と _alternative 関数、_value関数を使用して、この補完値に対してさらに説明文や依存する追加補完をかましています。まーこれらを使ってzsh特有の補完表現を実装したわけです。

_argument 関数は -- を含んだフラグを補完させるのが便利で、プラスしてサブコマンド kubectl "get" を補完にかますのに便利です。

_arguments  \    
${_global_flags[@]} \
"1: :{_alternative ':basic_cmd:__basic_cmd'}" \
'*:: :->args' && ret=0

https://github.com/nnao45/zsh-kubectl-completion/blob/9a3908a6c9166eae83eee427651edf62522fb4ed/_kubectl#L779

ここでは2行目で引数を渡す ${_global_flags[@]} は呼び出された段階で展開されてあらかじめ _gloal_flag 配列に詰めたフラグを補完に渡してます。

3行めは実行された補完対象から数えて0,1,,,のつまり kubectl getget に当たる位置に補完対象に入れます。この数字にですがとても便利で、入力されたのがフラグの場合はその位置は無視してくれます。 これはつまり kubectl --namespace test get と入力しても --namespace test は無視して数えてくれるわけです。

その後4行目は *:: :->args で、state変数というビルドイン変数にargsという値を入れます。それにより、これまたビルドイン変数のwords配列にサブコマンドである getapply などが詰まっていき、それにより処理が変えることに実現していくわけです。

またzshは面白くて例えば _argument に渡すフラグや _alternative で渡す補完値にはオプションを入れられて、

get:resource:__kube_get_resource--namespace[If present, the namespace scope for this CLI request]:namespaces:__kube_get_namespaces

みたいに定義してこれを補完として追加すると、 get を補完対象として追加したのちに、補完対象に依存補完値を追加していけたり、--namespaceフラグに説明文をつけ、さらにその後ろに補完させるように関数を叩いたりできます。このように柔軟に・・・闇に・・・組み立てていくわけですね。さ、俺が一から作りなおすしかねーなって思った気持ちもわかっていただけたかt(ry

ま、zshなんで、思い浮かんだことは基本的になんでもできます笑

作ってみて感想

  • 補完されるリソースに対してはその場で kubectl get を発行しているわけだけど、 kubectl get 自体はキャッシュ機能を持っているのでプラグイン側でキャッシュさせる事を考える必要がなかった。
  • ソースを見てみればわかるが全オプションは全部ハードコードである。なんでそんな頭おかしい事をしたかというと、以下のコマンドを見て欲しい。
$ kubectl describe --help                                                    
Show details of a specific resource or group of resources
Print a detailed description of the selected resources, including related resources such as events
or controllers. You may select a single object by name, all objects of that type, provide a name
prefix, or label selector. For example:
$ kubectl describe TYPE NAME_PREFIXwill first check for an exact match on TYPE and NAME PREFIX. If no such resource exists, it will
output details for every resource that has a name prefixed with NAME PREFIX.
Use "kubectl api-resources" for a complete list of supported resources.Examples:
# Describe a node
kubectl describe nodes kubernetes-node-emt8.c.myproject.internal
# Describe a pod
kubectl describe pods/nginx
# Describe a pod identified by type and name in "pod.json"
kubectl describe -f pod.json
# Describe all pods
kubectl describe pods
# Describe pods by label name=myLabel
kubectl describe po -l name=myLabel
# Describe all pods managed by the 'frontend' replication controller (rc-created pods
# get the name of the rc as a prefix in the pod the name).
kubectl describe pods frontend
Options:
--all-namespaces=false: If present, list the requested object(s) across all namespaces.
Namespace in current context is ignored even if specified with --namespace.
-f, --filename=[]: Filename, directory, or URL to files containing the resource to describe
--include-uninitialized=false: If true, the kubectl command applies to uninitialized objects.
If explicitly set to false, this flag overrides other flags that make the kubectl commands apply to
uninitialized objects, e.g., "--all". Objects with empty metadata.initializers are regarded as
initialized.
-R, --recursive=false: Process the directory used in -f, --filename recursively. Useful when you
want to manage related manifests organized within the same directory.
-l, --selector='': Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l
key1=value1,key2=value2)
--show-events=true: If true, display events related to the described object.
Usage:
kubectl describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME) [options]
Use "kubectl options" for a list of global command-line options (applies to all commands).

この謎フォーマットにスペースと記号だらけのhelp文からどうバリデーションして持ってくればいいのか全く思いつかなかったってだけです😭

  • もうしばらくしたら一応本家にPR送ってみようと思うが、ま、一から作ったもんで、当然弾かれそう・・・期待していないw
  • 補完関数の実装とデザインについては誰得感が凄まじい+異常に長くなるので省略したが、まぁ1日は語れるので(例えばグローバルオプションとローカルオプションの両立や、 --namespace の後の補完のやり方、 kubectl logsTYPE/NAME 形式でなんで補完出来るのか(後方補完プレフィックスとかを使ってだなry)?などw) 機会と需要があれば書くかもね。

参考文献について

https://github.com/zsh-users/zsh-completions … 他のzsherの猛者達がどうやって書いているか実装をみるのに必要。みんなハードコーディングしてんなって草が生えます。

http://zsh.sourceforge.net/Doc/Release/Completion-System.html … 公式のzshのドキュメント。非常にまとまっており、困ったらここ。全部英語だがもうzshの技術情報といえばここが原点となる。

http://amzn.asia/d/7pfFpjFzshの本 (エッセンシャルソフトウェアガイドブック)/著広瀬 雄二 … 超貴重な日本語で書かれたzshの本。2009年出版であるものの、現在のzsh5系の基本構文は網羅されている。とても読みやすく、zsher必携の聖書。

終わりに

よければ今回のプラグインにスターをいただければ開発の励みになりますのでよろしくお願いいたします🙇‍

--

--