YAML形式のKubernetesマニフェストをGo言語のオブジェクトにデシリアライズする

::: note info
これは [ZOZO Advent Calendar 2023](https://qiita.com/advent-calendar/2023/zozo) カレンダー Vol.5 の 3日目の記事です。
:::

本記事ではYAML形式のKubernetesマニフェストを読み込み、対応する型を持ったGo言語のオブジェクトにデシリアライズする方法を説明します。

`k8s.io/apimachinery`モジュールが提供する`yaml`パッケージの機能を利用することでこれを実現できます。  

# 実装例と解説
以下は、YAML形式で書かれたPodのマニフェストファイルを読み取り、`core/v1`の`Pod`型を持つオブジェクトにデシリアライズし、[`client-go`](https://github.com/kubernetes/client-go/tree/master)を使ってGo言語のオブジェクトからKubernetesのPodオブジェクトを作成する例になります。この例で作成するPodは次の2種類になります。

1. 元々のマニフェストのフィールド値を持つPod
2. 1のPod名を変更した新しいPod

```deserialize.go
package main

import (
	"context"
	"flag"
	"fmt"
	"os"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/serializer/json"
	"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/clientcmd"
)

var kubeconfig string

func main() {
	// YAMLマニフェストの読み込み
	path := "sample_pod.yaml"

	data, err := os.ReadFile(path)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// シリアライザの作成
	jsonSerializer := json.NewSerializerWithOptions(
		json.SimpleMetaFactory{},
		nil,
		scheme.Scheme,
		json.SerializerOptions{},
	)
	yamlSerializer := yaml.NewDecodingSerializer(jsonSerializer)

	// 読み込んだYAMLマニフェストのバイト列をcorev1.Pod型のオブジェクトにデシリアライズ
	var original corev1.Pod
	if _, _, err := yamlSerializer.Decode(data, nil, &original); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// Podを作成するクライアントの作成
	flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
	flag.Parse()
	config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
	clientset, _ := kubernetes.NewForConfig(config)

	// オブジェクトからsample-original Podを作成
	_, err = clientset.CoreV1().Pods("default").Create(context.TODO(), &original, metav1.CreateOptions{})

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// sample-originalオブジェクトのPod名を変更した新規オブジェクトを作成
	updated := original.DeepCopy()
	updated.ObjectMeta.Name = "sample-updated"

	// Pod名を変更した新規オブジェクトからsample-original Podを作成
	_, err = clientset.CoreV1().Pods("default").Create(context.TODO(), updated, metav1.CreateOptions{})

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

```
読み込み対象のYAMLマニフェストは以下になります。
```sample_pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: original-sample
  namespace: default
spec:
  containers:
  - name: sleep-container
    image: alpine
    command: ["sleep", "3600"]

```

以下では`deserialize.go`の実装について説明します。

## `[]bytes` → `corev1.Pod`へのデシリアライズ
byte列 → corev1.Pod型のオブジェクトへのデシリアライズは`yamlSerializer`オブジェクトの`Decode`メソッドの中で行われています。  
`yamlSerializer`型の`Decode`メソッドでは、引数で受け取ったYAMLをJSONに変換し、`apimachinery/pkg/runtime/serializer/json`パッケージの`Serializer`型の`Decode`メソッドを呼び出しています。  
Decode部分の実装が気になる方は、[`k8s.io/apimachinery/pkg/runtime/serializer/json`パッケージの`Decode`](https://github.com/kubernetes/apimachinery/blob/fa98d6eaedb4caccd69fc07d90bbb6a1e551f00f/pkg/runtime/serializer/json/json.go#L135)メソッドを参照してみてください。

`yaml.NewDecodingSerializer`関数を呼んで`yamlSerializer`を初期化する際に、引数に`jsonSerializer`を渡していることがわかります。  
また`jsonSerializer`を初期化する際には、`k8s.io/apimachinery/pkg/runtime/serializer/json`パッケージの`NewSerializerWithOptions`関数を呼び出しています。  
こちらの関数の実装は以下のようになっています。

```k8s.io/apimachinery/pkg/runtime/selializer/json/json.go
// NewSerializerWithOptions creates a JSON/YAML serializer that handles encoding versioned objects into the proper JSON/YAML
// form. If typer is not nil, the object has the group, version, and kind fields set. Options are copied into the Serializer
// and are immutable.
func NewSerializerWithOptions(meta MetaFactory, creater runtime.ObjectCreater, typer runtime.ObjectTyper, options SerializerOptions) *Serializer {
	return &Serializer{
		meta:       meta,
		creater:    creater,
		typer:      typer,
		options:    options,
		identifier: identifier(options),
	}
}
```

関数のドキュメントにも記載がある通り、3つ目の`typer`引数に`nil`でない値を渡すことで、作成されるオブジェクトが`Group-Version-Kind`のフィールドを持ちます。
こちらにはデシリアライズ先のリソースの`scheme`を指定しています。

`deserialize.go`では、`corev1.Pod`型の`scheme`を持った[`k8s.io/client-go/kubernetes/scheme`パッケージの`Scheme`変数](https://github.com/kubernetes/client-go/blob/84a6fe7e4032ae1b8bc03b5208e771c5f7103549/kubernetes/scheme/register.go#L80C5-L80C11)を渡しています。

[`Scheme`変数は`k8s.io/apimachinery/pkg/runtime/scheme`パッケージの`Scheme`型](https://github.com/kubernetes/apimachinery/blob/fa98d6eaedb4caccd69fc07d90bbb6a1e551f00f/pkg/runtime/scheme.go#L46)を持ちますが、こちらの型は`NewSerializerWithOptions`関数の`typer`引数が持つべき[`runtime.ObjectTyper`インターフェース](https://github.com/kubernetes/apimachinery/blob/fa98d6eaedb4caccd69fc07d90bbb6a1e551f00f/pkg/runtime/interfaces.go#L269)を満たしています。

以上が、YAML形式のKubernetesマニフェスト → corev1.Pod型のオブジェクトへの変換部分の説明になります。

## GoのオブジェクトからKubernetesオブジェクトを作成
`deserialize.go`の`// Podを作成するクライアントの作成`以降の処理では、`client-go`を使ってマニフェストから作成したオブジェクトからKubernetesのPodオブジェクトを作成しています。  
またマニフェストから作成したオブジェクトを元に別オブジェクトを作成し、一部フィールドの値を変更することで元々のマニフェストとは別のKubernetesのPodオブジェクトも作成しています。

`client-go`については、別記事の[client-go・api・apimachineryの機能と関係性](https://qiita.com/okmtz/items/0b6855d3656171579ba7)で触れているので参考にしてみてください。

## 実行結果
`deserialize.go`の実行結果は以下になっており、想定したKubernetes Podのオブジェクトが作成されていることがわかります。
```bash
go run deserialize.go --kubeconfig $HOME/.kube/config

k get po                                                                                                                                                                                                                       
NAME              READY   STATUS    RESTARTS   AGE
sample-original   1/1     Running   0          47m
sample-updated    1/1     Running   0          47m
```

# まとめ
本記事ではYAML形式のKubernetesマニフェストを読み込み、対応する型を持ったGo言語のオブジェクトにデシリアライズする方法を紹介しました。
`k8s.io/apimachinery`モジュールが提供する`yaml`パッケージの機能を利用し、次の手順によりこれを実現できました。

1. デシリアライズ先の型情報を持った`scheme`を用意
1. 用意した`scheme`を使って、`jsonSerializer`を作成
1. 作成した`jsonSerializer`から`yamlSerializer`を作成
1. `yamlSerializer`の`Decode`メソッドに、YAMLマニフェストのバイト列とデシリアライズ先のオブジェクトのポインタを指定して実行

入力がバイト列なので、マニフェストファイルに限らず、requestのbodyをデシリアライズする際など登場する場面は多そうかと思います。  

# 参考
- [Kubernetes Programming with Go](https://www.amazon.co.jp/Kubernetes-Programming-Go-Clients-Operators/dp/1484290259)