Go module の Dependency 問題を解決する

今日はGoの依存関係で躓いたので、次回躓かないようにいろいろ試してみる。

```bash
go mod init
```

の後次の簡単なk8s client のプログラムを書いて実行する。サンプルのまんまなのに、失敗する。

```go 
package main

import (
	"flag"
	"fmt"
	"path/filepath"
	"time"

	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

func main() {
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	flag.Parse()

	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		panic(err.Error())
	}

	clientset, err := kubernetes.NewForConfig(config)

	if err != nil {
		panic(err.Error())
	}
	for {
		pods, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{})
		if err != nil {
			panic(err.Error())
		}
		fmt.Printf("Tehre are %d pods in the cluster\n", len(pods.Items))

		namespace := "default"
		pod := "nginx"

		_, err = clientset.CoreV1().Pods(namespace).Get(pod, metav1.GetOptions{})
		if errors.IsNotFound(err) {
			fmt.Printf("Pod %s is namespace %s not found\n", pod, namespace)
		} else if statusError, isStatus := err.(*errors.StatusError); isStatus {
			fmt.Printf("Error getting pod %s in namespace %s: %v\n",
				pod, namespace, statusError.ErrStatus.Message)
		} else if err != nil {
			panic(err.Error())
		} else {
			fmt.Printf("Found pod %s in namespace %s\n", pod, namespace)
		}
		time.Sleep(10 * time.Second)

	}

}

```

_result_

```
$ go run main.go 
build command-line-arguments: cannot load github.com/googleapis/gnostic/OpenAPIv2: module github.com/googleapis/gnostic@latest found (v0.5.4), but does not contain package github.com/googleapis/gnostic/OpenAPIv2
```

なんだこれ？多分該当のバージョンが壊れていることが推察される。

今は解決しているわけだが、その場その場の解決策がわかっても意味がないので、実際に自分がやったことを書いてみる。

最初に思ったことは、`github.com/googleapis/gnostic/OpenAPIv2` を使っている依存関係を調べてそのライブラリをバージョンアップ、もしくはダウンすればよいということ。go では明確に依存関係のグラフみたいなものが出るのもが見つからなかった。`go mod why` が説明的にはそうなのだが、うまく動作していないように感じる。[StackOverflow](https://stackoverflow.com/questions/64875346/go-k8s-client-overriding-v11-0-0incompatible-dependency)も同じようなことを言っていた。

いろいろしたが、本来するべきだった手順は、きっと `go.mod` を見ることだろう。

```go
module simplearchitect.com

go 1.13

require (
	github.com/imdario/mergo v0.3.12 // indirect
	golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
	k8s.io/api v0.20.4 // indirect
	k8s.io/apimachinery v0.20.4
	k8s.io/client-go v11.0.0+incompatible
	k8s.io/klog v1.0.0 // indirect
	k8s.io/utils v0.0.0-20210305010621-2afb4311ab10 // indirect
)

```

// indirect というのがあるが、これは私が直接使っていないもの。`+incompatible` は go module に対応していないものなので怪しいところ。もし go module の依存関係がツリーで出てくるものがあれば最高なのだが、見つからなかったので、直接使っているライブラリの go.mod をGitHub のページに行って、それぞれ調査する。少なくとも、master/main の`github.com/googleapis/gnostic` のバージョンは `v0.4.1` であり、`v0.5.4` ではない。`k8s.io/client-go` `k8s.io/apimachinery` の双方が依存しているらしいが、どちらも、`v0.4.1` を使っている。不思議である。

では、次のステップとして、`github.com/googleapis/gnostic@v0.4.1` を試してみるが、同じエラーが出る。おそらく何かが `v0.5.4` を参照しているからであろう。次に考えたのが `v0.5.4` を削除してみようということ。`go mod tidy`  を使うと参照がキレた依存関係を整理してくれる。

```bash
$ go mod tidy
simplearchitect.com imports
        k8s.io/client-go/kubernetes imports
        k8s.io/client-go/discovery imports
        github.com/googleapis/gnostic/OpenAPIv2: module github.com/googleapis/gnostic@latest found (v0.5.4), but does not contain package github.com/googleapis/gnostic/OpenAPIv2
simplearchitect.com imports
        k8s.io/client-go/kubernetes imports
        k8s.io/client-go/kubernetes/typed/auditregistration/v1alpha1 imports
        k8s.io/api/auditregistration/v1alpha1: module k8s.io/api@latest found (v0.20.4), but does not contain package k8s.io/api/auditregistration/v1alpha1
simplearchitect.com imports
        k8s.io/client-go/kubernetes imports
        k8s.io/client-go/kubernetes/typed/settings/v1alpha1 imports
        k8s.io/api/settings/v1alpha1: module k8s.io/api@latest found (v0.20.4), but does not contain package k8s.io/api/settings/v1alpha1
```

うむ。これはなかなかわかりやすい。近づいている。例のライブラリは、`k8s.io/client-go` が使用している。では、`v11.0.0` はどんなバージョンだろうと[client-go のサイト]()にいくと、意味が分からないが

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/989fb756-a4d7-1098-852c-8331cb6edbfc.png)


最新 v12.0.0 とかあるのだが、（日付に注意）クリックすると、最新のバージョン全然ちゃうがな。
しかも、最新のプレリリースは、`v11.0.0+incompatible` を回避してくれて最新バージョンになるみたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/f769726f-86d6-f818-55f3-8ec69815d0c4.png)

結局のところ、解決策は `go get k8s.io/client-go@v0.20.4` だったのだがなぜそうなるんだろう？

# Major version suffix 

`go mod` の公式ドキュメントを読んでいると、Major Version Suffix というコンセプトが出てきた。これは、依存性の問題を避けるための解決策で、通常ライブラリが異なるバージョンを使っている場合、より上のバージョンが採用される。ところがそうだとバージョンのコンフリクトの問題が発生する。であるので、Goは メジャーバージョンが２以上の場合は、メジャーバージョンサフィックスをつけるように変更を行った。つまり、

`example.com/mod` は、`@v1.0.0` もしくは `@v0.0.0` 代のバージョンで使える表記であるが、メジャーバージョンが２以上の場合は、`example.com/mod/v2` として使う必要がある。これによって、メジャーバージョンが違うと、パスが異なるので、同じライブラリの異なるバージョンが存在できるようになる。つまり `v11.0.0.0+incompatible` の意味合いは、モジュールに対応していないからではなく、`v11.0.0.0` のライブラリは本来バージョンサフィックス付きで使われるべきなので、k8s.io/client-go/v11 としてインポートするべき（だけとできないのだろう）

インポート部分を以下のように変えてみる。

```go
import (
	"flag"
	"fmt"
	"path/filepath"
	"time"

	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/v11/kubernetes"
	"k8s.io/client-go/v11/tools/clientcmd"
	"k8s.io/client-go/v11/util/homedir"
)
```

_result_

```bash
$ go run main.go 
build command-line-arguments: cannot load k8s.io/client-go/v11/kubernetes: module k8s.io/client-go@latest found (v11.0.0+incompatible), but does not contain package k8s.io/client-go/v11/kubernetes
```

すると、どこかで見たようなエラーメッセージが出てきた。本来、v11 のサフィックスでアクセスできるようにするべきが、そうなってないので、コンテンツが無いように見えるということである。だから、おそらく `k8s.io/client-go` のプロジェクトは、バージョンサフィックスに対応するためにバージョン番号をv11.0.0 から、v0.20.0 に変更して、バージョンサフィックス問題を避けているのだろう。（多分）ところが、v11.0.0.0 は既にリリースされているので、普通に使うと`latest` ではそれがインポートされて `incompatible` 問題が発生したということであろう。

自分のメンテしているアプリが、メジャーバージョン上げたいときは、ディレクトリを掘る必要があるっぽい。

* [Go Modules: v2 and Beyond](https://blog.golang.org/v2-go-modules)


# まとめ

* メジャーバージョンが大きい場合は、現在の `go mod` ではバージョンサフィックスが必要なはずなので、無い場合は、そこが怪しい。
* 最初に確認するべきは`go.mod` 
* go mod tidy で、依存関係が壊れている個所を特定する
* 変なメジャーバージョンがある場合は、リポジトリの最新を見て、最新のものをインストールする。`go get k8s.io/client-go@v0.20.4` 等