これは ZOZO Advent Calendar 2023 カレンダー Vol.5 の 2日目の記事です。
本記事ではclient-goを利用してKubernetesのCustom ResourceのオブジェクトをGETする方法について紹介します。
client-goでDeploymentやPodといったKubernetesのデフォルトリソースを取得する場合、次のようにclientsetの初期化を行うと思います。
package main
import (
"context"
"flag"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
var kubeconfig string
func main() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
flag.Parse()
// kubeconfig fileの読み込み
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
// clientsetの初期化
clientset, _ := kubernetes.NewForConfig(config)
}
このように初期化されたclientsetは次のインターフェースを満たしており、各KubernetesリソースごとにClientを返すメソッドを持っています。
type Interface interface {
Discovery() discovery.DiscoveryInterface
...省略
AppsV1() appsv1.AppsV1Interface
AppsV1beta1() appsv1beta1.AppsV1beta1Interface
AppsV1beta2() appsv1beta2.AppsV1beta2Interface
...省略
CoreV1() corev1.CoreV1Interface
...省略
}
そのため次のように対応するオブジェクトの取得が可能です。
// Podオブジェクトの取得
pod, _ := clientset.CoreV1().Pods("hoge").Get(context.TODO(), "fuga", metav1.GetOptions{})
fmt.Println(pod.ObjectMeta.Name)
では、Custom Resourceのオブジェクトを取得するにはどのように実装できるのかというのが今回のテーマになります。
KubernetesではPodやDeloymentといったデフォルトリソース意外にたくさんのCustom Resourceが存在しています。一方で、これらを取得するための独自Clientは上記のコードで利用したclientsetには含まれていません。
client-goでCustom Resourceのオブジェクトを取得する場合、dynamic Clientを利用することができます。
dynamic Clientを利用すると、上記のclientsetでサポートしていないKubernetesリソースについても扱うことができます。
実験として、Sample Custom Resouceをデプロイし、dynamic Clientを使って取得してみます。
次の2つのKubernetesマニフェストをapplyしてCustom Resouceの作成を行なってください。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: samples.stable.example.com
spec:
group: stable.example.com
versions:
- name: v1alpha
# REST APIを介して提供されるのかのフラグ
served: true
storage: true
scope: Namespaced
names:
kind: Sample
plural: samples
singular: sample
shortNames: ["sp"]
versions:
- name: v1alpha
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
image:
type: string
message:
type: string
replicas:
type: integer
required:
- message
additionalPrinterColumns:
- name: message
type: string
description: message content which want to show
jsonPath: .spec.message
- name: AGE
type: date
jsonPath: .metadata.creationTimestamp
subresources:
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
labelSelectorPath: .status.labelSelector
apiVersion: "stable.example.com/v1alpha"
kind: Sample
metadata:
name: my-cr-sample-object
spec:
image: my-cr-image
message: "Hello Custom Resource"
replicas: 1
kubectlコマンドを叩いてCustom Resouce DefinitionとCustom ResouceであるSampleリソースのオブジェクトが取得できることを確認してください。
k get crd
NAME CREATED AT
samples.stable.example.com 2023-11-30T13:40:56Z
k get sample
NAME MESSAGE AGE
my-cr-sample-object Hello Custom Resource 3s
次のコードによりdynamic Clientを利用して上記のSampleリソースのオブジェクトを取得することができます。
package main
import (
"context"
"flag"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/dynamic"
)
var kubeconfig string
type Sample struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec SampleSpec `json:"spec"`
}
type SampleSpec struct {
Image string `json:"image"`
Message string `json:"message"`
Replicas int `json:"replicas"`
}
func main() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
flag.Parse()
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
client, _ := dynamic.NewForConfig(config)
gvr := schema.GroupVersionResource{
Group: "stable.example.com",
Version: "v1alpha",
Resource: "samples",
}
unstructuredObj, _ := client.Resource(gvr).Namespace("default").Get(context.TODO(), "my-cr-sample-object", metav1.GetOptions{})
sample := &Sample{}
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.Object, sample)
fmt.Println(sample.ObjectMeta.Name)
}
Podリソースのオブジェクトを取得した際との大きな差分は主に2つあります。
1つはclientからチェーンされる1つ目のメソッドが異なる点です。
// clientsetの場合
pod, _ := clientset.CoreV1().Pods("hoge").Get(context.TODO(), "fuga", metav1.GetOptions{})
// dynamic Clientの場合
obj, _ := client.Resource(gvr).Namespace("default").Get(context.TODO(), "my-cr-sample-object", metav1.GetOptions{})
Podの場合は元々client-go側でPod Kindに対応するGroup-Version-Resouce
を知っていますが、Sample Kindの場合は情報を持っていません。そのため、Resourceメソッドに直接Group-Version-Resouce
を渡しています。
2つ目は最終的に呼ばれているGetメソッドの戻り値の型が異なる点です。
podsオブジェクトに実装されているGetメソッドは次のようになっており、Intoメソッドにv1.Pod
型のポインタを渡して取得したレスポンスをv1.Pod
型にデコードしています。
// Get takes name of the pod, and returns the corresponding pod object, and an error if there is any.
func (c *pods) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.Pod, err error) {
result = &v1.Pod{}
err = c.client.Get().
Namespace(c.ns).
Resource("pods").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do(ctx).
Into(result)
return
}
func (r Result) Into(obj runtime.Object) error {
if r.err != nil {
// Check whether the result has a Status object in the body and prefer that.
return r.Error()
}
if r.decoder == nil {
return fmt.Errorf("serializer for %s doesn't exist", r.contentType)
}
if len(r.body) == 0 {
return fmt.Errorf("0-length response with status code: %d and content type: %s",
r.statusCode, r.contentType)
}
out, _, err := r.decoder.Decode(r.body, nil, obj)
if err != nil || out == obj {
return err
}
// if a different object is returned, see if it is Status and avoid double decoding
// the object.
switch t := out.(type) {
case *metav1.Status:
// any status besides StatusSuccess is considered an error.
if t.Status != metav1.StatusSuccess {
return errors.FromObject(t)
}
}
return nil
}
一方でdynamicResourceClientオブジェクトに実装されているGetメソッドは次のようになっています。
取得されたバイト列はruntime.Decode
関数に渡されていますが、この関数の内部ではunstructuredJSONScheme
のdecode
メソッドによりバイト列がUnstructured
型に変換されています。
func (c *dynamicResourceClient) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
if len(name) == 0 {
return nil, fmt.Errorf("name is required")
}
if err := validateNamespaceWithOptionalName(c.namespace, name); err != nil {
return nil, err
}
result := c.client.client.Get().AbsPath(append(c.makeURLSegments(name), subresources...)...).SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).Do(ctx)
if err := result.Error(); err != nil {
return nil, err
}
retBytes, err := result.Raw()
if err != nil {
return nil, err
}
uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, retBytes)
if err != nil {
return nil, err
}
return uncastObj.(*unstructured.Unstructured), nil
}
func (s unstructuredJSONScheme) decode(data []byte) (runtime.Object, error) {
type detector struct {
Items gojson.RawMessage `json:"items"`
}
var det detector
if err := json.Unmarshal(data, &det); err != nil {
return nil, err
}
if det.Items != nil {
list := &UnstructuredList{}
err := s.decodeToList(data, list)
return list, err
}
// No Items field, so it wasn't a list.
unstruct := &Unstructured{}
err := s.decodeToUnstructured(data, unstruct)
return unstruct, err
}
しかし、ここで返却されるUnstructured
構造体は次のように定義されておりObjectMeta
等のプロパティを持ちません。
type Unstructured struct {
// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
// map[string]interface{}
// children.
Object map[string]interface{}
}
こちらのUnstructured型のオブジェクトを具体的なリソースの型に変換しているのがruntime.unstructuredConverter
のFromUnstructured
メソッドです。
// FromUnstructured converts an object from map[string]interface{} representation into a concrete type.
// It uses encoding/json/Unmarshaler if object implements it or reflection if not.
func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error {
return c.FromUnstructuredWithValidation(u, obj, false)
}
関数のドキュメントにも記載がある通り、こちらの関数によりUnstructured
オブジェクトのObject
プロパティの値を指定された具体的な型に変換しています。
このように最終的にはmain.goに定義したSample型のオブジェクトとして値を取得することができ、main.goを実行すると次の結果を得ることができました。
go run main.go --kubeconfig ~/.kube/config
my-cr-sample-object