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を使っていくと良さそう。

Go製ソフトウェアの開発中にインポートしているパッケージを修正して手元で動作確認する方法

Goで書かれたソフトウェアを触っていると、そのソフトウェアがインポートしているパッケージを修正して手元で動作確認したいことがある。Go Modulesのreplaceを使うと手軽に実現できるので、その方法について記録しておく。

例としてhashicorp/vaultを挙げる。vaultは機能ごとにプラグインとして外部パッケージに切り出していて、例えばシークレットのKVはhashicorp/vault-plugin-secrets-kv だったりする。この時、vault-plugin-secrets-kv側を変更して、その変更をVaultとして動作確認したいというわけである。

ではどうするかというと、Go Modulesにあるreplaceを使う。

replace github.com/hashicorp/vault-plugin-secrets-kv => ../vault-plugin-secrets-kv

このようにgithub.com/hashicorp/vault-plugin-secrets-kvを相対パスで指定することで、修正内容が含まれるパッケージでビルドすることができる。

consul-templateでVaultのシークレットをソースにしている場合、更新してもすぐにはレンダリングされない場合がある

consul-templateでVaultのKVSからデータを取得してファイルに書き出したいことがある。Consulとconsul-templateを組み合わせた時と同様、Vault上のシークレットを更新するとすぐに反映されると思っていた。しかし、そうではなく、KVSの場合そのシークレットのリース期間(設定されていない場合は5分)の85%~95%が過ぎたあたりで更新を試みるようだった。

検証に使ったconsul-templateのコンフィグとテンプレートはこんな感じ。

config.hcl:

log_level = "debug"

vault {
  address = "http://localhost:8200"
  token = "roottoken"
  renew_token = false

  ssl {
    enabled = false
  }
}

template {
  source      = "./template.ctmpl"
  destination = "./test.txt"
  command     = "echo rendered"
}

template.ctmpl:

{{- with secret "kv-v2/foo" }}
a: {{ .Data.data.a }}
b: {{ .Data.data.b }}
{{- end }}

kv-v2/fooにデータを書き込んでおく。

$ vault kv put kv-v2/foo a=111 b=222
Key              Value
---              -----
created_time     2020-03-13T00:55:12.045597Z
deletion_time    n/a
destroyed        false
version          2

$ vault kv get kv-v2/foo
====== Metadata ======
Key              Value
---              -----
created_time     2020-03-13T00:55:12.045597Z
deletion_time    n/a
destroyed        false
version          2

== Data ==
Key    Value
---    -----
a      111
b      222

consul-templateを起動すると、test.txtに書き込まれる。

2020/03/13 00:55:24.545585 [INFO] (runner) creating watcher
2020/03/13 00:55:24.546436 [INFO] (runner) starting
2020/03/13 00:55:24.546455 [DEBUG] (runner) running initial templates
2020/03/13 00:55:24.546462 [DEBUG] (runner) initiating run
2020/03/13 00:55:24.546520 [DEBUG] (runner) checking template dc39c8fc3e10d93709e0aabdf4a1b566
2020/03/13 00:55:24.546827 [DEBUG] (runner) missing data for 1 dependencies
2020/03/13 00:55:24.546846 [DEBUG] (runner) missing dependency: vault.read(kv-v2/foo)
2020/03/13 00:55:24.546853 [DEBUG] (runner) add used dependency vault.read(kv-v2/foo) to missing since isLeader but do not have a watcher
2020/03/13 00:55:24.546858 [DEBUG] (runner) was not watching 1 dependencies
2020/03/13 00:55:24.546860 [DEBUG] (watcher) adding vault.read(kv-v2/foo)
2020/03/13 00:55:24.546870 [DEBUG] (runner) diffing and updating dependencies
2020/03/13 00:55:24.546874 [DEBUG] (runner) watching 1 dependencies
2020/03/13 00:55:24.657740 [DEBUG] (runner) receiving dependency vault.read(kv-v2/foo)
2020/03/13 00:55:24.657783 [DEBUG] (runner) initiating run
2020/03/13 00:55:24.657791 [DEBUG] (runner) checking template dc39c8fc3e10d93709e0aabdf4a1b566
2020/03/13 00:55:24.658390 [DEBUG] (runner) rendering "./template.ctmpl" => "./test.txt"
2020/03/13 00:55:24.672384 [INFO] (runner) rendered "./template.ctmpl" => "./test.txt"
2020/03/13 00:55:24.672419 [DEBUG] (runner) appending command "echo rendered" from "./template.ctmpl" => "./test.txt"
2020/03/13 00:55:24.672428 [DEBUG] (runner) diffing and updating dependencies
2020/03/13 00:55:24.672439 [DEBUG] (runner) vault.read(kv-v2/foo) is still needed
2020/03/13 00:55:24.672445 [INFO] (runner) executing command "echo rendered" from "./template.ctmpl" => "./test.txt"
2020/03/13 00:55:24.672552 [INFO] (child) spawning: echo rendered
rendered
2020/03/13 00:55:24.678989 [DEBUG] (runner) watching 1 dependencies
2020/03/13 00:55:24.679010 [DEBUG] (runner) all templates rendered
2020/03/13 00:55:24.679049 [DEBUG] (cli) receiving signal "child exited"

ここで、Vault側を更新するとconsul-templateがtest.txtを更新してくれるのかと思ったが、そうではなかった。

Consul-template not update file when data in Vault is updated によると、Vaultはシークレットの更新をウォッチするブロッキングクエリをサポートしていないらしい。ではどうやって更新するかというと、シークレットのリース期間の半分が過ぎると更新を試みるらしい。なるほど、ではリース期間から待ち時間を算出するロジックを探してみよう。

https://github.com/hashicorp/consul-template/blob/v0.24.1/dependency/vault_common.go#L126-L167 のleaseCheckWaitが待ち時間を算出する関数のようだ。

// leaseCheckWait accepts a secret and returns the recommended amount of
// time to sleep.
func leaseCheckWait(s *Secret) time.Duration {
	// Handle whether this is an auth or a regular secret.
	base := s.LeaseDuration
	if s.Auth != nil && s.Auth.LeaseDuration > 0 {
		base = s.Auth.LeaseDuration
	}

	// Handle if this is a certificate with no lease
	if certInterface, ok := s.Data["certificate"]; ok && s.LeaseID == "" {
		if certData, ok := certInterface.(string); ok {
			newDuration := durationFromCert(certData)
			if newDuration > 0 {
				log.Printf("[DEBUG] Found certificate and set lease duration to %d seconds", newDuration)
				base = newDuration
			}
		}
	}

	// Ensure we have a lease duration, since sometimes this can be zero.
	if base <= 0 {
		base = int(VaultDefaultLeaseDuration.Seconds())
	}

	// Convert to float seconds.
	sleep := float64(time.Duration(base) * time.Second)

	if vaultSecretRenewable(s) {
		// Renew at 1/3 the remaining lease. This will give us an opportunity to retry
		// at least one more time should the first renewal fail.
		sleep = sleep / 3.0

		// Use some randomness so many clients do not hit Vault simultaneously.
		sleep = sleep * (rand.Float64() + 1) / 2.0
	} else {
		// For non-renewable leases set the renew duration to use much of the secret
		// lease as possible. Use a stagger over 85%-95% of the lease duration so that
		// many clients do not hit Vault simultaneously.
		sleep = sleep * (.85 + rand.Float64()*0.1)
	}

	return time.Duration(sleep)
}

標準的なシークレットの場合、LeaseDurationがあればそれを、なければVaultDefaultLeaseDurationをリース期間として使っている(VaultDefaultLeaseDurationは5分)。そして、シークレットがリニュー可能かどうかでロジックは異なるが、待ち時間を計算して返却する。

kv-v2/fooをみると、lease_durationは0なのでVaultDefaultLeaseDurationが使われる。

{
  "request_id": "c8ffdb88-1b66-10d1-b417-3061e843914f",
  "lease_id": "",
  "lease_duration": 0,
  "renewable": false,
  "data": {
    "data": {
      "a": "111",
      "b": "222",
      "ttl": "60s"
    },
    "metadata": {
      "created_time": "2020-03-13T01:02:53.685381Z",
      "deletion_time": "",
      "destroyed": false,
      "version": 4
    }
  },
  "warnings": null
}

なお、証明書の場合は証明書の発行から失効までの時間をリース期間としている。

consul-templateでVaultと連携してあれこれできて便利だが、更新のタイミングには注意しないといけないということがわかった。

Ubuntuでユーザーディレクトリを再作成するには mkhomedir_helper を使うと良い

Ubuntuでユーザーディエレクトリを消した後に/etc/skelをベースに再作成したい時があると思うのだけど、そういう時は mkhomedir_helper が使える。

Usage: /sbin/mkhomedir_helper <username> [<umask> [<skeldir>]]

こういう感じでユーザー名を指定するとその名前でユーザーディレクトリが作られる。

iPhoneのパスワード自動補完で1Passwordが使えることに気づいたので設定した

iPhoneのSafariから1Passwordを使う時、これまではシェアボタンから起動していたのだけど、結構面倒。どうにかならんかなと思っていたら、自動補完設定に気づいたので設定した。

設定方法は1Passwordアプリが説明してくれるのでその通りにする。

すると、ページを開いたタイミングですぐにサジェストされる。ログインする場合はボタンを押せばFaceIDで認証してログインできる。

これはもっと早く知りたかった…