client-go: DynamicClient/DynamicInformerの使い方

# TL;DR

- DynamicClient/DynamicInformerの使い方を紹介しました
- CRDなどを扱うときクライアントライブラリがない場合はDynamicClient/DynamicInformerを使えばライブラリを生成しなくても操作できる

# はじめに

一昨年の[アドベントカレンダーで記載した](https://qiita.com/shmurata/items/f75ff02a8096e8cfd6cf)クラスタオートスケーラの独自プロバイダを実装する際に、ライブラリの依存関係で問題となり DynamicClient/DynamicInformer を利用したのと意外とDynamicClientに関する情報がなかったので忘備録を兼ねて利用方法をまとめておこうと思います。

# DynamicClient/DynamicInformer とは

正式な定義や命名があるわけではないと思いますが、この記事では https://github.com/kubernetes/client-go/tree/master/dynamic にある Kubernetes のクライアントライブラリの一部のライブラリのことを指しています。
通常の Informer や client は typed なクライアントとなっていて、標準リソース（Pod,Service，ReplicaSetなど）向けのクライアントライブラリしかありません。
CRD などで生成した独自のオブジェクトなどは client-go では提供されていないため、コードジェネレータなどで生成するのが一般的ですが、DynamicInformerはそれらを用意せずとも利用できるクライアントライブラリです。

# ドキュメント

ドキュメントと言えるものはこのくらいしか見当たりませんでした :cry: 

https://github.com/kubernetes/client-go/tree/master/examples/dynamic-create-update-delete-deployment

# DynamicClientの使い方

上のサンプルソースにありますが、通常のclientと同じように kubeconfig オブジェクトを用意したら以下のようにクライアントを生成します。

```go
client, err := dynamic.NewForConfig(config)
```
https://github.com/kubernetes/client-go/blob/master/examples/dynamic-create-update-delete-deployment/main.go#L67

typed との違いはこのあとで、操作するリソースを GVR (GroupVersionResource) で指定して GRUD のメソッドを呼び出します。上記のサンプルでは Deployment を利用しているので以下のようになります。

```go
deploymentRes := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
```
https://github.com/kubernetes/client-go/blob/v0.20.0-beta.2/examples/dynamic-create-update-delete-deployment/main.go#L67


そしてオブジェクトを作成する場合は この GVR を以下のように指定して

```go
result, err := client.Resource(deploymentRes).Namespace(namespace).Create(context.TODO(), deployment, metav1.CreateOptions{})
```
https://github.com/kubernetes/client-go/blob/v0.20.0-beta.2/examples/dynamic-create-update-delete-deployment/main.go#L112

のようにすることで作成できます。
ただこのとき deployment の変数に入れているのは unstructured.Unstructured の型で作る必要があります。

Deployment を表現する場合は https://github.com/kubernetes/client-go/blob/v0.20.0-beta.2/examples/dynamic-create-update-delete-deployment/main.go#L69-L108 のようになります。

dynamicclientを利用する場合は基本的にUnstructuredのオブジェクトを操作する必要があります。
ただ何もなしにこれを操作するのはかなり大変なのでヘルパー関数が用意されています。

https://github.com/kubernetes/apimachinery/tree/master/pkg/apis/meta/v1/unstructured

例えばunstructured.UnstructuredのDeploymentオブジェクトからspec.replicasを取得する場合は[NestedInt64](https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/unstructured/helpers.go#L116)をつかって以下のようにすることで取得することができます。

```
replicas, found, err := unstructured.NestedInt64(obj, "spec", "replicas")
```

# DynamicInformer

DynamicClientは上記のサンプルがありますが、DynamicInformerについては全く情報が見つかりませんでした。
そのためソースコードから使い方を調べる必要がありました。

みんな同じようなことを考えるようで、クラスタオートスケーラのClusterAPIのクラウドプロバイダーを見ると、DynamicInformerを利用して実装していました。
そのため、この実装を元に使い方を紹介していきたいと思います。

Informerのオブジェクトの作り方は以下のようにsharedInformerFactoryを作成していきます。Dynamicの場合はDynamic用の関数が用意されているので[NewFilteredDynamicSharedInformerFactory](https://github.com/kubernetes/client-go/blob/v0.20.0-beta.2/dynamic/dynamicinformer/informer.go#L42)を利用して生成します。

```go
managementInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(managementClient, 0, metav1.NamespaceAll, nil)
```
ref: https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.19.1/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_controller.go#L322

上のリンクを踏むと通常のInformerを使った例もすぐ上にあるので違いがすぐにわかりますね。

生成できたらWatchするオブジェクトを指定とイベントを受け取る関数を設定していきます。

```go
	gvrMachineSet := schema.GroupVersionResource{
		Group:    CAPIGroup,
		Version:  CAPIVersion,
		Resource: resourceNameMachineSet,
	}
	machineSetInformer := managementInformerFactory.ForResource(gvrMachineSet)
	machineSetInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{})
```
ref: https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.19.1/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_controller.go#L356-L362

ここではHandleFuncsの指定はありませんが、指定する場合は通常のInformerと同じように以下のように指定することができます。

```go
				return &cache.ResourceEventHandlerFuncs{
					AddFunc: func(obj interface{}) {
						rcvCh <- obj.(*unstructured.Unstructured)
					},
				}
```
https://github.com/kubernetes/client-go/blob/v0.20.0-beta.2/dynamic/dynamicinformer/informer_test.go#L156-L160

これで準備ができたのであとは通常のInformerと同様にStartさせます。

```go
	c.managementInformerFactory.Start(stopCh)
```
ref: https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.19.1/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_controller.go#L173


あとはよく忘れそうになるCacheの同期待ちの処理です。これも通常のInformerと同様に実施すれば問題ありません。

```go

	syncFuncs := []cache.InformerSynced{
		c.nodeInformer.HasSynced,
		c.machineInformer.Informer().HasSynced,
		c.machineSetInformer.Informer().HasSynced,
	}
	if c.machineDeploymentsAvailable {
		syncFuncs = append(syncFuncs, c.machineDeploymentInformer.Informer().HasSynced)
	}

	klog.V(4).Infof("waiting for caches to sync")
	if !cache.WaitForCacheSync(stopCh, syncFuncs...) {
		return fmt.Errorf("syncing caches failed")
	}
```
ref: https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.19.1/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_controller.go#L175-L187

あとは通常のInformerと同様にWorkQueueなどを利用してReconcile Loopを処理できるように作成すれば良いと思います。

# 苦労する点

DynamicClient/DynamicInformer を利用してオブジェクトの取得や更新は上記のように簡単にできますが、unstructuredオブジェクトを操作するのがやや煩雑で特にスライスを絡めた場合がかなり煩雑になります。

例えばDeploymentのオブジェクトで `deployment.spec.template.spec.containers.env` にCOMPANYに`Z Lab Corporation`を加えようとすると以下のようになります。

```go
    containers, found, err := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
    if err != nil {
        fmt.Printf("err: %v", err)
    }
    if !found {
        fmt.Printf("not found spec.template.spec.containers")
    }
    for _, container := range containers {
        name, found, err := unstructured.NestedString(container.(map[string]interface{}), "name")
        if err != nil {
            fmt.Printf("err: %v", err)
        }
        if !found {
            fmt.Printf("not found spec.template.spec.containers.name")
        }
        if name == "web" {
            var envList []interface{}
            env := map[string]interface{}{
                "name":  "COMPANY",
                "value": "Z Lab Corporation",
            }
            envList = append(envList, env)
            err := unstructured.SetNestedSlice(container.(map[string]interface{}), envList, "env")
            if err != nil {
                fmt.Printf("err: %v", err)
            }
            break
        }
    }
```
*上記のコードは動作確認できていないので細部に誤りがあるかもしれません。*

# まとめ

この記事ではDynamicClient/DynamicInformerの使い方を紹介しました。
DynamicClient/DynamicInformerはCRD用のライブラリを用意しなくても良い、細かなCRDの更新への対応が必要なくなるという面はありますが、型のチェックが不十分になる、利用方法が煩雑でバグを生みやすいなどのデメリットもあるので使う場合は慎重に検討したほうが良さそうに思います。おそらく多くの場合ではクライアントライブラリを用意したほうが良いでしょう。