MacOSのAlacrittyで複数ウィンドウ表示する

僕はターミナルの画面を複数並べて作業することがあるのだけど、Alacrittyだとそれができないと思っていた。しかし、今日調べてみるとどうやらできるらしい。

https://github.com/alacritty/alacritty/pull/3461

このPRによると、SpawnNewInstanceというアクションを使うと新しいインスタンスを作れる(要は新しいウィンドウが起動してくる)ようだ。なので、Cmd+Nにこのアクションを紐付けるとウィンドウを複数並べることができるというわけ。早速設定して、ほぼ不便な点はなくなって最高。なお、0.5.0でデフォルトキーバインドに追加されるっぽい。

DragonflyのCDN機能を使って効率的なイメージダウンロードを実現する

Dragonflyという、Alibabaが中心となって開発されているイメージ・ファイルのP2Pベース分散配布ツールがあります。今回はDragonflyのCDN機能について調査・検証しました。なお、検証に使ったDragonflyのバージョンはv1.0.0です。

Dragonflyとは何か

Dragonflyが何か、一言で述べるとP2Pベースのイメージやファイルの配布ツールと言えます。大規模な環境において、ファイル転送の効率をあげ、ネットワーク帯域の利用効率を最大化することを目的としています。What Is Dragonfly? によるとアリババでは月に20億回Dragonflyが呼び出され、3.4PBのデータ転送が行われているとのこと。Dragonfly自体は汎用的に作られているが、わかりやすい利用事例としてはコンテナイメージの配布が挙げられます。例えば、Kubernetesのノード台数が増えれば増えるほど、それぞれのノードでイメージのpullが行われ、帯域や効率へ影響が出ます。イメージのサイズが大きくなると、イメージpullのパフォーマンスはさらに悪化する可能性もあります。Dragonflyを使うと単純なDocker利用より57倍のスループットを実現し、レジストリの帯域を最大99.5%節約できると書かれていました。

なぜ必要か

Dragonflyがどのような機能を持つのか、What Is Dragonfly? から紹介します。アリババの内部ではさらに多くの機能があるそうです。

  • P2Pベースのファイル配布
    • ファイル転送にP2Pの技術を使うことで、各ピアが持つ帯域リソースを最大限活用する。また、IDC間の帯域を節約できる。
  • あらゆる種類のコンテナ技術に対して、非侵襲的にサポートする
    • Dragonflyは様々なイメージ配布のためのコンテナ技術をシームレスにサポートする
  • ホストレベルの速度制限
    • wgetやcurlのようなダウンロードが持つタスクレベルの速度制限に加えて、Dragonflyはホスト全体の速度制限を提供する
  • パッシブCDN
    • CDNの機構を備えており、リモートからのダウンロードの繰り返しを回避できる
  • 強力な整合性
    • Dragonflyはユーザーがmd5のようなチェックサムを提供しなくても全てのダウンロードファイルに整合性があることを確認できる
  • ディスクの保護と効率的なIO
    • ディスク容量の事前チェックや同期の遅延、最適な順序でのファイルブロック書き込み、net-read/disk-writeの分離など
  • 高いパフォーマンス
    • SuperNode(Dragonflyのコンポーネントの1つ)は完全に閉制御されていて、データベースや分散キャッシュを必要とせずに高いパフォーマンスでリクエストを処理する
  • 例外の自動隔離
    • Dragonflyは自動的に例外ノード(peerもしくはSuperNode)を隔離し、ダウンロードの安定性を改善する
  • ファイルソースへのプレッシャーがない
    • ソースからダウンロードするSuperNodeはごくわずかで良い
  • 標準的なHTTPヘッダのサポート
    • HTTPヘッダを介した認証情報の送信をサポート
  • Registry Authの効果的な同時実行制御
    • RegistryAuthサービスへのプレッシャーを軽減する
  • シンプルで簡単に使える
    • 必要な設定はほとんどない

なるほど、イメージは掴めてきました。では実際に触って行ってみましょう。今回はCDN機能について主に試します。

一般的なファイルダウンロードの場合

適当なKubernetesクラスターを用意して、Dragonflyをインストールします。helm chartがあるのでこれを使うと比較的楽です。

NAME                                       READY   STATUS    RESTARTS   AGE
d7y-dragonfly-dfclient-2q7jw               1/1     Running   0          3d
d7y-dragonfly-dfclient-4w77g               1/1     Running   0          3d
d7y-dragonfly-dfclient-6hhw4               1/1     Running   0          3d
d7y-dragonfly-dfclient-7xtw6               1/1     Running   0          3d
d7y-dragonfly-dfclient-vpzgb               1/1     Running   0          3d
d7y-dragonfly-dfclient-xxnj5               1/1     Running   0          3d
d7y-dragonfly-supernode-6b96dfc674-wwrls   1/1     Running   0          3d

適当なPodに入って、dfgetコマンドでファイルダウンロードしてCDNが動作しているのかを確かめてみます。

$ k exec -it d7y-dragonfly-dfclient-vpzgb bash
bash-4.4#
bash-4.4#
bash-4.4# time /opt/dragonfly/df-client/dfget --url https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz --output ./kubernetes-server-linux-amd64.tar.gz --node d7y-drago
nfly-supernode --verbose
--2020-04-20 03:31:06--  https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz
dfget version:1.0.0
workspace:/root/.small-dragonfly
sign:29-1587353466.555
client:10.100.0.156 connected to node:d7y-dragonfly-supernode:8002
start download by dragonfly...
download SUCCESS cost:42.585s length:363231097 reason:0

real    0m42.754s
user    0m3.072s
sys     0m6.552s
bash-4.4#
bash-4.4#
bash-4.4# rm ./kubernetes-server-linux-amd64.tar.gz
bash-4.4# time /opt/dragonfly/df-client/dfget --url https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz --output ./kubernetes-server-linux-amd64.tar.gz --node d7y-dragonfly-supernode --verbose
--2020-04-20 03:31:56--  https://dl.k8s.io/v1.18.1/kubernetes-server-linux-amd64.tar.gz
dfget version:1.0.0
workspace:/root/.small-dragonfly
sign:45-1587353516.929
client:10.100.0.156 connected to node:d7y-dragonfly-supernode:8002
start download by dragonfly...
download SUCCESS cost:19.322s length:363231097 reason:0

real    0m19.405s
user    0m1.976s
sys     0m1.000s

1回目と2回目でダウンロードにかかった時間がかなり違うことが確認できました。dfgetコマンドで手軽に扱えるのは便利です。

アーキテクチャとしてはこのようになっています。supernodeとdfgetの2コンポーネントが使われて、dfgetを使うとsupernode経由でファイルをダウンロードします。また、dfgetはP2Pクライアント(ピア)として動き、ピア間でのブロック転送も行われます。supernodeはCDNとして働き、各ピア間のブロック転送のスケジューリングなどを行います。

コンテナイメージpullの場合

次に、コンテナイメージのpullについて試します。なお、コンテナランタイムとしてはdockerではなくcontainerdを使っています。

dfdaemonの設定はこのような感じ。dfdaemonはデフォルトでindex.docker.ioをレジストリミラーとして扱うようになっています。また、proxiesでURLにblobs/sha256.*が含まれている場合はdfgetを使ってリクエストする、という設定にしました。

dfget_flags:
- --verbose
- -f
- Expires&OSSAccessKeyId&Signature
proxies:
- regx: blobs/sha256.*
  use_https: true
node:
  - 10.233.60.241:8002

containerdにregistry mirrorの設定をします。docker.ioのミラーエンドポイントとしてdfdaemonを指定します。

    [plugins.cri.registry]
      [plugins.cri.registry.mirrors]
        [plugins.cri.registry.mirrors."docker.io"]
          endpoint = ["http://127.0.0.1:65001"]

これで準備はOK。イメージをpullしてみます。初回と2回目で約7.3秒の差があ離ました。centosイメージなのでそもそもそんなに大きくないのですが、大きなイメージを扱うときには役に立つであろうことがわかります。

$ time sudo crictl pull centos
Image is up to date for sha256:470671670cac686c7cf0081e0b37da2e9f4f768ddc5f6a26102ccd1c6954c1ee

real    0m23.898s
user    0m0.028s
sys     0m0.020s
$ sudo crictl rmi centos
Deleted: docker.io/library/centos:latest
$ time sudo crictl pull centos
Image is up to date for sha256:470671670cac686c7cf0081e0b37da2e9f4f768ddc5f6a26102ccd1c6954c1ee

real    0m16.698s
user    0m0.032s
sys     0m0.024s

アーキテクチャはこのようになっていて、supernodeとdfgetに加えて、dfdaemonが使われます。dfdaemonはdockerのようなコンテナランタイムからのpushやpullリクエストをインターセプトし、必要に応じてdfgetで処理します。

プライベートレジストリからのイメージをpullする

前節ではDockerHubからのイメージpullをDragonfly経由で行いました。しかし、場合によってはプライベートレジストリからのイメージpullをDragonfly経由で行いたいのではないでしょうか(僕がやりたい)。これを行う方法は2つあります。一つ目は、dfdaemonをHTTP Proxyとして扱う方法。もう一つは、dfdaemonをregistry mirrorとして扱う方法です。

HTTP Proxyとして扱う

Use Dfdaemon as HTTP Proxy for Docker Daemon に方法が記載されています。Dockerはregistry-mirrorsフラグを使ったプライベートレジストリの設定に対応していないため、この方法を使う必要があります。しかしdfdaemonをdockerで起動していると、docker再起動時にdfdaemonが止まり、dockerがdfdaemonに繋がらず起動しませんでした。systemdなどを使って起動すれば問題ないかと思います。

registry mirrorとして扱う

dfdaemonをHTTP Proxyとして使うのは若干扱いにくいと感じたため、別の策として検証しました。containerdはプライベートレジストリをregistry mirrorとして設定できるため、HTTP Proxyの設定がなくても動作するはずです。

dfdaemonの設定はこのようにします。dfdaemon自身がレジストリミラーとして参照するURLを変更します。

registry_mirror:
  remote: https://your.private.registry
dfget_flags:
- --verbose
- -f
- Expires&OSSAccessKeyId&Signature
proxies:
- regx: blobs/sha256.*
  use_https: true
verbose: true
node:
  - 10.233.60.241:8002

containerdのレジストリ設定はこのようにします。containerd、レジストリの認証情報もtomlにまとめて設定できて便利です。

    [plugins.cri.registry]
      [plugins.cri.registry.auths]
        [plugins.cri.registry.auths."http://127.0.0.1:65001"]
          username = "username"
          password = "password"
          auth = "XXXXXXXXXXXXXXXXXXXXXXXX"
      [plugins.cri.registry.mirrors]
        [plugins.cri.registry.mirrors."your.private.registry"]
          endpoint = ["http://127.0.0.1:65001"]

これで、イメージのダウンロード時にdfdaemonはdfgetを用いてsupernodeへリクエストします。1回目と2回目でpullの時間が変わるかどうかみてみましょう。

$ time sudo crictl pull your.private.registry/image:0.0.2
Image is up to date for sha256:4358591c9919667bf6a8baca78c2e85f1ffe82700ca81e2550a63c532e49c142

real    1m30.891s
user    0m0.040s
sys     0m0.036s
$
$ sudo crictl rmi your.private.registry/image:0.0.2
Deleted: your.private.registry/image:0.0.2
$ time sudo crictl pull your.private.registry/image:0.0.2
Image is up to date for sha256:4358591c9919667bf6a8baca78c2e85f1ffe82700ca81e2550a63c532e49c142

real    1m31.800s
user    0m0.040s
sys     0m0.024s

あれ、ほとんど変わりませんね… Dragonflyは帯域制御などの機能もあるので、その辺りの設定を変えてみました。今回変えたのは以下。

  • dfdaemon
    • ratelimit (dfgetのlocallimitにセットされる。ダウンロード時のネットワーク帯域の上限を指定)
  • supernode
    • systemReservedBandwidth

設定を変更したところ、初回のpullは1m58s、2回目のpullは59sと変化が感じられました(初回は遅くなってるじゃんという)。しかしcontainerd単体でpullすると40sくらいで終わるので、もう少し調べたいところです。構成要素が複数あり、その設定がどこに影響するのかを把握するのが難しいなと感じました。特にdfdaemonは設定と内部の構造体が分かれているので、追いかけるのが大変です。

まとめ

DragonflyのCDN機能について、一般的なファイルダウンロードとコンテナイメージPullについて調査と検証を行いました。大規模なクラスターでイメージのPullが帯域を圧迫していたり、巨大なイメージのPullに時間がかかって困っているような場合は試してみるといいかもしれません。目玉であろうP2P機能については今回は試さなかったので、今後試していきたいですね。また、複数のレジストリについてDragonflyでミラーすることができるのかについても検証しようと考えています(やりたい)。

オマケ:元々はJavaで書かれていたらしい

DragonflyはGoで書かれています。しかし、ドキュメントを読んでいると、Java関係の記述が割と出てくる。なぜかなと思ったら、元々はJavaで書かれていたのがGoで書き直されたようです。0.4.0で完全にGoになったとのことです。普段よく読み書きしているのがGoなので、同じ感覚で扱えるのはありがたいです。

kubeletが使うコンテナランタイムをdockerからcontainerdに変更する

kubeletが使うコンテナランタイムはこれまでdockerを使っていたのだが、別のものを試してみようということでcontainerdに変更してみた。

containerdをインストールする

containerdはaptでインストールできる(パッケージ: containerd)。また、containerdが使うLow Level Runtimeであるruncも同時にインストールされる。インストールした後、以下のコマンドを実行して設定のデフォルト値を /etc/containerd/config.toml に書き込み、systemctl start containerd で起動する。

$ containerd config default

kubeletからcontainerdを使う

kubeletの引数に以下を追加して再起動することで、containerdを使うようになる。

 --container-runtime=remote
 --container-runtime-endpoint=/run/containerd/containerd.sock

CRIのおかげで、これだけでランタイムを切り替えることができてとても便利。

ノード上で動いているコンテナ一覧を見るには

これまではdocker psを実行してそのノード上で動いているコンテナの一覧を表示していた。containerdの場合はどうすればいいのかというと、2つ方法がある。

まずはcontainerdに付属しているctrコマンド。namespaceフラグでk8s.ioを指定することで、kubeletによって作成されたコンテナの一覧を見ることができる。

$ sudo ctr --namespace k8s.io containers list
CONTAINER                                                           IMAGE                                                                                                    RUNTIME
24ff8b4bb5d6cdfdc5311aa44716a53fa7cd578da76b125e063660e8459ea96f    k8s.gcr.io/metrics-server-amd64:v0.3.1                                                                   io.containerd.runtime.v1.linux

しかし、コンテナのIDとイメージ、ランタイムしか見えずトラブルシュートに使うには物足りない。そこで、もう1つの方法としてcrictlを使うことができる。

https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md

crictlはCRIに互換性のあるコンテナランタイム向けのCLIツールである。crictl psでコンテナ一覧を見ることができる。表示項目はdocker psに近く、使いやすいだろう。また、docker psコマンドでコンテナ一覧を見るとコンテナ名が長くなり読みにくいが、crictlだと素朴に名前が表示されるので視認性も高い。しかし、pauseコンテナは表示されないのでその点は注意(見えないことで困ることがあるのかはよく分からない)。

$ sudo crictl ps
CONTAINER ID        IMAGE               CREATED             STATE               NAME                       ATTEMPT             POD ID
24ff8b4bb5d6c       61a0c90da56e7       22 hours ago        Running             metrics-server             0                   68dfed1214a0a

crictlはexecやlogsなど、ノード内でコンテナを見るときに使うコマンドも備えているので基本的にはこれを使い、必要に応じてctrコマンドを使うのが良さそう。なお、runcにもlistコマンドがありコンテナ一覧を表示できそうだったが何も見えなかった。

containerdをkubernetesから使ってみたい方、参考になれば幸いです。

reference

ClusterAPI v0.3.0の変更点

2020年3月11日(JST)にClusterAPI v0.3.0がリリースされました。本記事では、v0.3.0でどのような変更があったのかについて、v0.3.0のリリースノートを眺めてみます。

v0.3.0で実装されたプロポーザル

Kubeadm based control plane

Proposal: https://github.com/kubernetes-sigs/cluster-api/blob/master/docs/proposals/20191017-kubeadm-based-control-plane.md

このプロポーザルは、ClusterAPI がコントロールプレーンマシンを単一概念として管理するための新しいプロセスの概要です。アップグレード、スケールアップ、コントロールプレーンマシンが使うイメージの変更(例:AMI)を含んでいます。

2019年の時点では、各InfrastructureProviderでコントロールプレーン管理が実装されていました。ブートストラップの処理がClusterAPI Bootsrap Provider Kubeadm(CABPK)として共通化されたのと同様、各プロバイダーでコントロールプレーンの管理について実装しなくても良いように共通化するもののようです。BootstrapProviderとあわせて、InfrastructureProvider側で実装すべきコードが減るのは良さそうですね。

Clusterctl v2

Proposal: https://github.com/kubernetes-sigs/cluster-api/blob/master/docs/proposals/20191016-clusterctl-redesign.md

これまでclusterctlコマンドは各プロバイダーで微妙に異なる物が提供されており、混乱の元になっていました。また、clusterctlのスコープが広く、複雑になっていたようです。v2ではDay1におけるエクスペリエンスをスコープとし、最適化されたCLIを提供するように変更されています。

Machine Health Checking and Remediation

Proposal: https://github.com/kubernetes-sigs/cluster-api/blob/master/docs/proposals/20191030-machine-health-checking.md

マシンのヘルスチェック自動化と、異常なノードの修復を行う提案です。これにより、クラスタ管理のオーバーヘッド削減、マシン障害に対する回復力を高めることができます。

実装としてはMachineHealthChecker(MHC)があり、これは異常なノードを提供するマシンを削除することでクラスターノードを正常に保とうとします。

MachinePool (experimental)

Proposal: https://github.com/kubernetes-sigs/cluster-api/blob/master/docs/proposals/20190919-machinepool-api.md

AWSのAuto Scale GroupやGCPのManaged Instance Group、AzureのVirtual Machine Scale Setなど、クラウドサービスのマシンプール機能を使えるようにする提案です。全てのインフラストラクチャプロバイダーがマシンプール機能を持つわけではないので、オプションとして提供されています。AWSなどを使っている場合は使えるというわけですね。

v0.3.0-rc.3からの変更点

新機能

  • KubeadmControlPlaneのReconcile時にCoreDNSイメージ情報を更新するようになりました (#2574)
  • KubeadmControlPlaneの更新時にkubeproxyデーモンセットも更新するようになりました (#2559)
  • KubeadmControlPlaneの更新時にetcdのアップグレードも行われるようになりました (#2579)
  • clusterctl: 隔離された環境の全てのイメージを上書きできるようになりました (#2586)
  • clusterctl: cert-managerイメージの上書きが許可されました (#2558)
  • MachineHealthCheck の修正ロジックが追加されました (#2494)

バグフィックス

  • KubeadmControlPlaneにおいて、etcdの設定をバリデーションする際のN P(nil pointer exception)を修正しました (#2616)

その他

  • upgradeAfter フィールドが変更可能になりました (#2604)
  • KubeadmControlPlaneのspec.versionをミュータブルにできるようになりました (#2582)
  • コントロールプレーンのEtcdがローカルにもエクスターナルにもない際のバリデーションを追加しました (#2598)
  • KubeadmControlPlane内でバージョニングを一致させるための修正 (#2624)
  • Kubeadm v1beta1 typesからプライベートAPIが削除されました (#2605)
  • etcdの変更を行うため、コントロールプレーンのwebhookバリデーションが緩和されました (#2553)
  • 削除されたオブジェクトをcontroller.Watchに渡すようになりました (#2576)
  • リポジトリ全体でGoモジュールのクリーンアップ (#2552)
  • KubeadmControlPlane nodesにNoExecute taintのチェックが追加されました (#2550)
  • Metal3プロバイダーのリポジトリが変更されました (#2540)
  • cmd-clusterctl-api/tests: gomegaのインポート方法が標準化されました (#2615)
  • tests: gomegaのインポート方法が標準化されました(フォローアップ) (#2589)
  • tests: gomega/ginkgoのインポート方法が標準化されました (#2571)

所感

v0.2.0から引き続き、統合できる物は統合して個々のInfrastructureProviderを開発しやすくしたり、CLIツールの責務を整理して扱いやすくしたりという変更が目立ちます。また、MachineHealthCheckやMachinePoolのような新しい仕組みも追加されており、まだまだ進化してるな〜と感じました。v0.3.0も一度触ってみようと思います。

envTestを使って、kubernetesのAPIを使うテストを行う

KubernetesのAPIを呼び出すコードを書いていてそこにロジックが含まれる時、テストを行いたい。こういう時、envTestを使うと手軽にetcdとapiserverを起動してテストできる。

ドキュメントは https://book.kubebuilder.io/reference/testing/envtest.html

まずはシンプルに動かしてみる。testEnv.Start() でetcdとapiserverを起動し、そのapiserverに繋がるrest.Configを得る。rest.Configを使ってkubernetes.Clientsetを作成し、ConfigMapを作成したり取得するというもの。テストの最後にはtestEnv.Stop()でetcdとapiserverを停止している。

package main

import (
	"fmt"
	"github.com/k0kubun/pp"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	"testing"
)

func Test_envtest(t *testing.T) {
	testEnv := &envtest.Environment{}

	restConfig, err := testEnv.Start()
	if err != nil {
		t.Error(err)
	}

	clientset, err := kubernetes.NewForConfig(restConfig)
	if err != nil {
		t.Error(err)
	}

	cm := &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-configmap",
			Namespace: "default",
		},
		Data: map[string]string{
			"foo": "bar",
		},
	}

	fmt.Printf("cm: %s\n", pp.Sprint(cm))
	_, err = clientset.CoreV1().ConfigMaps("default").Create(cm)
	if err != nil {
		t.Error(err)
	}
	got, err := clientset.CoreV1().ConfigMaps("default").Get("test-configmap", metav1.GetOptions{})
	if err != nil {
		t.Error(err)
	}

	fmt.Printf("got: %s\n", pp.Sprint(got))
	if got.Name != cm.Name {
		t.Errorf("missmatch: got = %v, want = %v", got.Name, cm.Name)
	}

	err = testEnv.Stop()
	if err != nil {
		t.Error(err)
	}
}

実行するとこのようになる。作成元のConfigMapと取得したConfigMapをそれぞれ出力しているが、UIDなどが付与されておりリソースとして登録されていることが確認できる。

$ go test ./testenv_test.go -v
=== RUN   Test_envtest
cm: &v1.ConfigMap{
  TypeMeta: v1.TypeMeta{
    Kind:       "",
    APIVersion: "",
  },
  ObjectMeta: v1.ObjectMeta{
    Name:              "test-configmap",
    GenerateName:      "",
    Namespace:         "default",
    SelfLink:          "",
    UID:               "",
    ResourceVersion:   "",
    Generation:        0,
    CreationTimestamp: v1.Time{
      Time: 1-01-01 00:00:00 UTC,
    },
    DeletionTimestamp:          (*v1.Time)(nil),
    DeletionGracePeriodSeconds: (*int64)(nil),
    Labels:                     map[string]string{},
    Annotations:                map[string]string{},
    OwnerReferences:            []v1.OwnerReference{},
    Initializers:               (*v1.Initializers)(nil),
    Finalizers:                 []string{},
    ClusterName:                "",
    ManagedFields:              []v1.ManagedFieldsEntry{},
  },
  Data: map[string]string{
    "foo": "bar",
  },
  BinaryData: map[string][]uint8{},
}
got: &v1.ConfigMap{
  TypeMeta: v1.TypeMeta{
    Kind:       "",
    APIVersion: "",
  },
  ObjectMeta: v1.ObjectMeta{
    Name:              "test-configmap",
    GenerateName:      "",
    Namespace:         "default",
    SelfLink:          "/api/v1/namespaces/default/configmaps/test-configmap",
    UID:               "478835ca-73df-11ea-b65a-acde48001122",
    ResourceVersion:   "43",
    Generation:        0,
    CreationTimestamp: v1.Time{
      Time: 2020-04-01 15:09:00 Local,
    },
    DeletionTimestamp:          (*v1.Time)(nil),
    DeletionGracePeriodSeconds: (*int64)(nil),
    Labels:                     map[string]string{},
    Annotations:                map[string]string{},
    OwnerReferences:            []v1.OwnerReference{},
    Initializers:               (*v1.Initializers)(nil),
    Finalizers:                 []string{},
    ClusterName:                "",
    ManagedFields:              []v1.ManagedFieldsEntry{},
  },
  Data: map[string]string{
    "foo": "bar",
  },
  BinaryData: map[string][]uint8{},
}
--- PASS: Test_envtest (6.17s)
PASS
ok      command-line-arguments  8.066s

client-goのパッケージはモッキングするのも大変だろうから、envtestを使っていくと良さそう。