client-goでDynamic Clientを使用して複数のマニフェストを適用する

client-goで単体のリソースを作成する場合は以下のように明示的に対象のclientを作成することで行うことができます。

func main() {
    ...
    deployment := &appsv1.Deployment{}
    deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)
    result, err := deploymentsClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
}

しかし、このやり方だと以下のような複数の種類リソースがまとまったyamlや、どのような種類を扱うか不明なときには利用するのが難しくなります。

apiVersion: v1
kind: Namespace
metadata:
  name: example
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: example

そこでDynamic Clientを使うと、このような状況でもclient-goでリソースの作成をすることができます。

Dynamic Client

Dynamic Clientを使用して上記のyamlを適用していきます。

Dynamic Clientの初期化

通常のClientを作成するときと同じようにDynamic Clientに必要な変数を初期化します。

import (
    "k8s.io/apimachinery/pkg/api/meta"
    "k8s.io/client-go/discovery"
    "k8s.io/client-go/dynamic"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "k8s.io/client-go/restmapper"
    "k8s.io/client-go/tools/clientcmd"
)

var (
    client          kubernetes.Interface
    discoveryClient discovery.DiscoveryInterface
    mapper          meta.RESTMapper
    dynamicClient   dynamic.Interface
    config          *rest.Config
)

type Dynamic struct {
    Client    dynamic.Interface
    Discovery discovery.DiscoveryInterface
    Mapper    meta.RESTMapper
}

func InitK8s() error {
    conf, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
    if err != nil {
        return err
    }
    config = conf

    clientset, err := kubernetes.NewForConfig(conf)
    if err != nil {
        return err
    }
    client = clientset

    discoveryClient = clientset.Discovery()

    groupResources, err := restmapper.GetAPIGroupResources(discoveryClient)
    if err != nil {
        return err
    }
    mapper = restmapper.NewDiscoveryRESTMapper(groupResources)

    dyn, err := dynamic.NewForConfig(config)
    if err != nil {
        return err
    }
    dynamicClient = dyn

    return nil
}

ポイントとしては RESTMapper を作成するときにDynamic Clientを構築するために必要な情報をAPI Serverから取得するため、毎回実行してしまうとサーバーに負荷をかけてしまうので予め作成しておく必要があります。

全体の流れ

k8s.io/apimachinery に付属する関数を使用することでリソース単位のオブジェクトに分割できます。

分割したリソースごとにClientを構築してマニフェストを適用していきます。

import (
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/util/yaml"
)

func main() {
    ...
    dynamic := Dynamic{}
    manifest := "..."
    decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(manifest), 100)
    for {
        var rawObj runtime.RawExtension
        if err := decoder.Decode(&rawObj); err != nil {
            break
        }

        obj := &unstructured.Unstructured{}
        client, err := dynamic.NewClient(rawObj.Raw, obj) 
        res, err := dynamic.Apply(client, obj)
        ...
   }
}

Dynamic Clientの構築

まずは分割したリソースからclientが適切なAPIにアクセスするための情報をパースして、Dynamic Clientを構築します。

func (d Dynamic) NewClient(data []byte, obj *unstructured.Unstructured) (dynamic.ResourceInterface, error) {
    dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
    _, gvk, err := dec.Decode(data, nil, obj)
    if err != nil {
        return nil, err
    }

    mapping, err := d.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
    if err != nil {
        return nil, err
    }

    if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
        if obj.GetNamespace() == "" {
            obj.SetNamespace(metav1.NamespaceDefault)
        }
        return d.Client.Resource(mapping.Resource).Namespace(obj.GetNamespace()), nil
    } else {
        return d.Client.Resource(mapping.Resource), nil
    }
}

上記コード gvkgvk.String() を出力すると apps/v1, Kind=DaemonSet のような情報が出力されます。
これがDynamic Clientを構築するために必要な情報です。
この情報をもとに先程作成した RESTMapper を経由してAPI情報を取得してDynamic Clientを構築します。

Create

Dynamic Clientの構築が完了したので、リソースを作成します。

kubectl create 相当のものはこのようになります。

func (d Dynamic) Create(ctx context.Context, client dynamic.ResourceInterface, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
    return client.Create(ctx, obj, metaV1.CreateOptions{
        FieldManager: "example",
    })
}

また、 kubectl apply 相当のものはこのようになります。

func (d Dynamic) Apply(ctx context.Context, client dynamic.ResourceInterface, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
    data, err := json.Marshal(obj)
    if err != nil {
        return nil, err
    }

    return client.Patch(ctx, obj.GetName(), types.ApplyPatchType, data, metaV1.PatchOptions{
        FieldManager: "example",
    })
}

このようにしてDynamic Clientを構築することで、複数のリソースがまとまったマニフェストをclient-goを使用して適用することができます。