client-goを利用してKubernetesのnamespaceを切り替える対話式CLIツールを作成した。

## 今回作ったツール - kns
![kns.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279529/29412d97-a66c-e1eb-990c-fb9386a72eff.gif)

Kubernetesによく触るのですが、Golangを書いたことがなかったので、ずっと触りたかったClient-goを使ってnamespaceを切り替えるツールを作成しました。

https://github.com/jlandowner/kns

namespaceを切り替えるツールとしては<code>kubectx</code>に同梱されている<code>kubens</code>が有名かと思いますが、bashで書かれているので、Golangで作るのに丁度いい題材となりました。
https://github.com/ahmetb/kubectx/blob/master/kubens


使い勝手はgifをみていただければと思いますが、シンプルで割りかし良いと思っています。
この手のツールは作っても使わないことが多いので・・・

今回のツール作成の際のポイントを少しまとめたいと思います。
KubernetesでGolangに興味を持ったエンジニアのご参考になれば幸いです。


## client-goでKubernetes API Serverにアクセス
基本的にはclient-goのリポジトリにあるサンプルコードを参考に作りました。
https://github.com/kubernetes/client-go/tree/master/examples

こちらに<code>in-cluster-client-configuration</code>と<code>out-of-cluster-client-configuration</code>があるのですが、今回はコマンドラインツールのため<code>out-of-cluster-client-configuration</code>を参考にします。

基本的にクライアントのkubeconfigの情報を利用してAPI Serverに繋ぎに行きます。
（通常kubeconfigは<code>~/.kube/config</code>にあります。）

namespace名を取得するまでの簡単流れは以下の通りです。
大事なところだけ見ると結構シンプルです。

```go

import (
        "os"
        "gopkg.in/yaml.v2"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/client-go/kubernetes"
        "k8s.io/client-go/tools/clientcmd"
)

// kubeconfigのパスを取得
var kubeconfigPath *string
*kubeconfigPath = filepath.Join(os.Getenv("HOME"), ".kube", "config")

// kubeconfigを元にclientcmd.BuildConfigFromFlagsでconfigオブジェクトを取得
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

// clientsetなるオブジェクトを取得。（こいつで各種API操作が可能です）
clientset, err := kubernetes.NewForConfig(config)

// 今回はnamespaceのリストを以下のコマンドで取得
nss, err := clientset.CoreV1().Namespaces().List(metav1.ListOptions{})

// namespaceの名前を取得。
ns_name = nss.Items[i].ObjectMeta.Name
```

取得した<code>ns</code>の持っている属性はgodocから見れます
https://godoc.org/k8s.io/api/core/v1#Namespace
ただし、Kubernetesのオブジェクトは共通して<code>typemeta</code>と<code>objectmeta</code>という2つの属性を持っていますので、一つ一つ調べる必要はありません。

・Typemeta
https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta

・Objectmeta
https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta

Kubernetesのマニフェストでお馴染みの属性たちです。今回は名前が欲しかったので、<code>ObjectMeta.Name</code>で取得しました。

podやserviceなど他のオブジェクトの操作方法はgodocをご参照ください。
（以下はcore/v1のリンクです。）
https://godoc.org/k8s.io/client-go/kubernetes/typed/core/v1


## Golangでyamlの読み書き
kubeconfigはyamlで書かれているので、yamlを読み書きする必要があります。



client-goのパッケージにkubeconfigを操作するものがあるのですが、今回はGolangの勉強をかねているので、あえてyamlのパース部分を実装しました。

```go

import (
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"

	"gopkg.in/yaml.v2"
)

// Kubeconfigの構造体を定義
type Kubeconfig struct {
	APIVersion     string `yaml:"apiVersion"`
	Kind           string
	Clusters       []interface{}
	Contexts       []Context
	CurrentContext string `yaml:"current-context"`
	Preferences    interface{}
	Users          interface{}
}

// Contextの構造体を定義
type Context struct {
	Context struct {
		Cluster   string
		Namespace string
		User      string
	}
	Name string
}

// read kubeconfig file
func readKubeconfig(kubeconfigPath string) (kConfig Kubeconfig) {
	buf, err := ioutil.ReadFile(kubeconfigPath)
	if err != nil {
		panic(err)
	}

	// yaml to struct
	err = yaml.Unmarshal(buf, &kConfig)
	if err != nil {
		panic(err)
	}
	return kConfig
}

// write kubeconfig to file
func writeKubeconfig(kubeconfigPath string, kConfig Kubeconfig) (err error) {
	err = nil

	// struct to yaml
	out, err := yaml.Marshal(kConfig)
	if err != nil {
		panic(err)
	}
	// Write kubeconfig file
	err = ioutil.WriteFile(kubeconfigPath, out, 0600)
	if err != nil {
		panic(err)
	}
	return err
}

func main() {
        ...
        // 読み込み
        kConfig := readKubeconfig(*kubeconfigPath)
        ...
        // 構造体の中身を編集
        kConfig.Contexts[i].Context.Namespace = namespace
        ...
        // 書き込み
        err := writeKubeconfig(*kubeconfigPath, kConfig)
        ...
}
```

構造体を定義すると<code>yaml.Unmarshal()</code>で、構造体の名前だけでうまくyamlをパースしてくれます。反対も<code>yaml.Marshal()</code>で簡単にファイル出力可能な形式にしてくれます。

ただしちょっといくつかの制約があり、少し調べました。
　・属性名は大文字から始める。
　・構造体で使えないハイフンなどは読み取ってくれないので、その場合は`yaml:"current-context"`で対象を指定する。

```go
type Kubeconfig struct {
    APIVersion     string `yaml:"apiVersion"`
    Kind           string
    Clusters       []interface{}
    Contexts       []Context
    CurrentContext string `yaml:"current-context"`
    Preferences    interface{}
    Users          interface{}
}
```

interface{}はまだ使いこなせていないのですが、型指定を曖昧にできるものという認識で、今回は使わない属性はinterface{}にしました。（合っていなければご指摘ください。。）

## 対話式のコマンドライン
最後に対話式コマンドラインについてです。
インタラクティブな操作がしたかったのですが、思いのほか簡単に書けました。

こちらの記事を参考に、bufioパッケージのscannerというものを使いました。
https://blog.linkbal.co.jp/1971/

```go
import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strconv"
)

// show dialog to ask
func askNamespaceNum(max int) (i int, qerr error) {
	fmt.Print("** Which namespace do you want to switch? (exit: q)\n")
	fmt.Print("Select[n] => ")
	i = 99
	qerr = nil

	// scannerの取得 
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		// 入力内容の取得
		input := scanner.Text()

		if atoi, err := strconv.Atoi(input); err == nil {
			i = atoi
			if 0 <= i && i < max {
				break
			}
			fmt.Print("Select[n] => ")
		} else if input == "q" {
			qerr = errors.New("quit")
			// breakすると入力待ち終了
			break
		} else {
			fmt.Print("Select[n] => ")
		}
	}

	if err := scanner.Err(); err != nil {
		panic(err)
	}
	return i, qerr
}
```

Podの一覧から`kubectl exec`したり、`kubectl logs`したり出来たらいいなと色々思いつきます。


## 最後に
Kubernetesやコンテナ周りはほとんどGolangの世界なので、それらの深い理解のためにGolangの学習は大きいと思います。
Golang自体非常に読みやすいので、少しわかるだけでもさらにソースコードリーディングが捗ります。

今後はCustom ControllerやOperatorを作成したいです。