Tanzu CLI の Plugin を作成してみる。

この記事は、「[TUNA-JP Advent Calendar 2023](https://qiita.com/advent-calendar/2023/tuna-jp)」の5日目のエントリです。

突然ですが、Tanzu CLI の Plugin を作成して Tuna とじゃんけんなどしてみます。

手順は、下記を参考に進めます。
* Tanzu CLI Plugin Implementation Guide
  https://github.com/vmware-tanzu/tanzu-cli/blob/main/docs/plugindev/README.md

# 今回の環境

VMware Photon OS 5.0 を利用します。
* 下記の OVA をデプロイしてあります。
  * https://packages.vmware.com/photon/5.0/GA/ova/photon-hw15-5.0-dde71ec57.x86_64.ova
* 初回ログイン（root / changeme）して、root パスワードは変更してあります。
* ネットワークは設定済みで、インターネット接続できるようにしてあります。

```
# cat /etc/photon-release
VMware Photon OS 5.0
PHOTON_BUILD_NUMBER=dde71ec57
```

# 1. 開発環境の準備

OS の RPM パッケージを最新化して、再起動しておきます。
```
# tdnf update -y
# reboot
```

ビルドで必要になる RPM をインストールしておきます。
```
# tdnf install tar git make go glibc-devel binutils linux-api-headers -y
```

Docker のサービスを起動しておきます。これは、プラグインの Publish 処理のあたりで必要になります。
```
# systemctl start docker
# systemctl enable docker
```

# 2. OS ユーザの作成（オプション）

作業用のユーザ（demo）を作成します。ちなみに、root ユーザのまま作業しても問題ありません。
```
# useradd -m demo
# passwd demo
# usermod -aG docker demo
```

作成したユーザにスイッチします。
```
# su - demo
$
```

vim のエディタ設定を調整しておきます。
```
$ echo 'set clipboard=unnamed,autoselect' >> ~/.vimrc
```


# 3. Tanzu CLIのインストール

Tanzu CLI のバイナリ ファイルをダウンロードします。
```
$ curl -OL https://github.com/vmware-tanzu/tanzu-cli/releases/download/v1.1.0/tanzu-cli-linux-amd64.tar.gz
```

ファイルを展開します。
```
$ tar zxvf tanzu-cli-linux-amd64.tar.gz
v1.1.0/
v1.1.0/tanzu-cli-linux_amd64
```

ホーム ディレクトリ配下に、コマンドを格納するディレクトリ（~/bin）を作成しておきます。
```
$ mkdir ~/bin
$ export PATH=~/bin:$PATH
```

Tanzu CLI をインストールします。
```
$ install v1.1.0/tanzu-cli-linux_amd64 ~/bin/tanzu
```

tanzu コマンドが実行できるようになったことを確認します。
```
$ tanzu version
version: v1.1.0
buildDate: 2023-11-01
sha: d0679f5a
arch: amd64
```

PATH 環境変数と、tanzu コマンドの Bash 補完機能の設定を、.bash_profile ファイルに追記して永続化します。
```
$ echo 'export PATH=~/bin:$PATH' >> ~/.bash_profile
$ echo 'source <(tanzu completion bash)' >> ~/.bash_profile
```

.bash_profile を読み込むために、作業用ユーザで再ログインしておきます。
```
$ exit
# su - demo
```

tanzu plugin list コマンドを実行して、まだ Plugin がインストールされていないことを確認しておきます。
初回のみ、下記のように EULA と CEIP の応答（下記の★あたり）を求められます。順に次のように回答しています。
* VMware General Terms（EULA）の承諾 → Yes
* CEIP:Customer Experience Improvement Program → No
```
$ tanzu plugin list
? You must agree to the VMware General Terms in order to download, install, or
use software from this registry via Tanzu CLI. Acceptance of the VMware General
Terms covers all software installed via the Tanzu CLI during any Session.
“Session” means the period from acceptance until any of the following occurs:
(1) a change to VMware General Terms, (2) a new major release of the Tanzu CLI
is installed, (3) software is accessed in a separate software distribution
registry, or (4) re-acceptance of the General Terms is prompted by VMware.

To view the VMware General Terms, please see https://www.vmware.com/vmware-general-terms.html.

If you agree, the essential plugins (required by the tanzu cli) will be automatically installed.

Note: this prompt can be avoided by running "tanzu config eula accept".

Do you agree to the VMware General Terms?
 Yes★

==
? VMware's Customer Experience Improvement Program ("CEIP") provides VMware with
information that enables VMware to improve its products and services and fix
problems. By choosing to participate in CEIP, you agree that VMware may collect
technical information about your use of VMware products and services on a
regular basis. This information does not personally identify you.

For more details about the program, please see https://www.vmware.com/trustvmware/ceip.html.

Note: this prompt can be avoided by setting the environment variable
TANZU_CLI_CEIP_OPT_IN_PROMPT_ANSWER to "yes" or "no".

Do you agree to participate in the Customer Experience Improvement Program?
 No★

==
Standalone Plugins
  NAME  DESCRIPTION  TARGET  VERSION  STATUS
```


# 4. Builder Plugin のインストール

builder Plugin をインストールします。

```
$ tanzu plugin install builder
[i] The tanzu cli essential plugins have not been installed and are being installed now. The install may take a few seconds.
[i] Installing plugins from plugin group 'vmware-tanzucli/essentials:v1.0.0'
[i] Installing plugin 'telemetry:v1.1.0' with target 'global'

[i] Installing plugin 'builder:v1.1.0' with target 'global'
[ok] successfully installed 'builder' plugin
```

# 5. Plugin の作成
まず Plugin の雛形を作成して、Go 言語で Cobra による CLI コマンドを追加します。

## 5-1. Plugin の雛形の作成
Plugin 開発のための Git リポジトリを作成します。
コマンド実行後、GitHub / GitLab どちらかの形式を選択します。
今回は GitHub を選択しています。（★のところにて）
```
$ tanzu builder init tanzu-cli-tunajp
[i] The tanzu cli essential plugins have not been installed and are being installed now. The install may take a few seconds.
[i] Installing plugins from plugin group 'vmware-tanzucli/essentials:v1.0.0'
[i] Installing plugin 'telemetry:v1.1.0' with target 'global'

? choose a repository type GitHub★
2023-12-04T10:45:07Z [ok] successfully created repository
```

作成されたディレクトリに移動します。
```
$ cd tanzu-cli-tunajp/
```

下記のようにファイルが作成されています。
```
$ ls
CODEOWNERS  common.mk  go.mod  Makefile  plugin-tooling.mk  README.md
```

tuna-jp という Plugin を作成します。tanzu builder cli add-plugin コマンドを実行すると、今回は「tuna-jp plugin.」と入力します。
```
$ tanzu builder cli add-plugin tuna-jp
? provide a description tuna-jp plugin.
2023-12-04T10:47:49Z [ok] successfully created plugin
```

下記のように、Plugin のテンプレートになるファイル一式が作成されます。
```
$ ls cmd/plugin/tuna-jp/
main.go  README.md  test
```

ここから git commit しながら進めるので、Git のユーザ情報を設定しておきます。
```
$ git config user.name "ユーザ名"
$ git config user.email "メールアドレス"
```

初期状態のファイル一式を Commit しておきます。
```
$ git add -A
$ git commit -m "Initialize plugin repository"
```

作成する Plugin のバージョンを、git tag で付与します。
```
$ git tag v0.0.1
```

make を実行します。
```
$ export GOPATH=~/go
$ make gomod
```

make に成功した時点で、また Commit を実行してきます。
```
$ git add -A
$ git commit -m "Configure go.mod and go.sum"
```

## 5-2. CLI コマンドの追加
main.go ファイルを編集します。
```
$ vi cmd/plugin/tuna-jp/main.go
```

下記のように Go 言語で Plugin を開発します。
```go:main.go
package main

import (
        "fmt"
        "github.com/spf13/cobra"
        "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
        "github.com/vmware-tanzu/tanzu-plugin-runtime/log"
        "github.com/vmware-tanzu/tanzu-plugin-runtime/plugin"
        "github.com/vmware-tanzu/tanzu-plugin-runtime/plugin/buildinfo"
        "math/rand"
        "os"
        "time"
)

var descriptor = plugin.PluginDescriptor{
        Name:        "tuna-jp",
        Description: "tuna-jp plugin.",
        Target:      types.TargetGlobal, // <<<FIXME! set the Target of the plugin to one of {TargetGlobal,TargetK8s,TargetTMC}
        Version:     buildinfo.Version,
        BuildSHA:    buildinfo.SHA,
        Group:       plugin.ManageCmdGroup, // set group
}

func main() {
        p, err := plugin.NewPlugin(&descriptor)
        if err != nil {
                log.Fatal(err, "")
        }

        // 新しいCobraコマンドを作成
        var tunaCmd = &cobra.Command{
                Use:   "get",
                Short: "Prints TUNA-JP Message",
                Long:  `Prints TUNA-JP Message to the standard output.`,
                Run: func(cmd *cobra.Command, args []string) {
                        fmt.Println("TUNA-JP Advent Calendar 2023")
                },
        }

        // janken コマンドの作成
        var jankenCmd = &cobra.Command{
                Use:   "janken",
                Short: "Play janken.",
                Long:  `Play janken. Use --hand with one of 'o', 'v', 'w'.`,
        }

        var hand string
        jankenCmd.Flags().StringVarP(&hand, "hand", "", "", "Your hand (o: rock, v: scissors, w: paper)")

        jankenCmd.Run = func(cmd *cobra.Command, args []string) {
                if hand != "o" && hand != "v" && hand != "w" {
                        fmt.Println("Invalid hand. Please use one of 'o', 'v', 'w'.")
                        return
                }

                // ランダムな手を生成
                rand.Seed(time.Now().UnixNano())
                opponent := []string{"o", "v", "w"}[rand.Intn(3)]

                // 結果を出力
                fmt.Printf("Your hand: %s, Tuna's fin: %s\n", hand, opponent)
        }

        // コマンドをプラグインに追加
        p.AddCommands(
                tunaCmd,
                jankenCmd,
        )
        if err := p.Execute(); err != nil {
                os.Exit(1)
        }
}
```


## 5-3. Plugin のローカル ビルド

作成した Plugin をビルドしてみます。ここでは、作業マシンのアーキテクチャ（linux/amd64）のバイナリのみが生成されます。
```
$ make plugin-build-local
（省略）
2023-12-04T11:08:01Z [i] ========
2023-12-04T11:08:01Z [i] saving plugin manifest...
2023-12-04T11:08:01Z [ok] successfully built local repository
```

下記のように、Plugin のバイナリ ファイルが作成されます。
```
$ ls -l artifacts/plugins/linux/amd64/global/tuna-jp/v0.0.1/
total 6132
-rwxr-x--- 1 root root 6271655 Dec  4 11:08 tanzu-tuna-jp-linux_amd64
drwxr-x--- 2 root root    4096 Dec  4 11:08 test
```

# 6. Plugin の動作確認
作成した Plugin が利用できるか確認してみます。

## 6-1. Plugin をインストールしてみます。

```
$ tanzu plugin install all --local-source ./artifacts/plugins/linux/amd64
[i] Installing plugin 'tuna-jp:v0.0.1' with target 'global'
[ok] successfully installed all plugins
```

## 6-2. Plugin に含まれるコマンドの実行
tanzu tuna-jp get コマンドを実行すると、「TUNA-JP」と表示されるはずです。
```
# tanzu tuna-jp get
TUNA-JP Advent Calendar 2023
```

help を表示してみます。
```
# tanzu tuna-jp janken -h
Play janken. Use --hand with one of 'o', 'v', 'w'.

Usage:
  tanzu tuna-jp janken [flags]

Flags:
      --hand string   Your hand (o: rock, v: scissors, w: paper)
  -h, --help          help for janken
```

TUNA とじゃんけんでもして平和な気持ちになってください。
```
$ tanzu tuna-jp janken --hand o
Your hand: o, Tuna's fin: w
```
```
$ tanzu tuna-jp janken --hand v
Your hand: v, Tuna's fin: w
```
```
$ tanzu tuna-jp janken --hand w
Your hand: w, Tuna's fin: o
````

## 6-3. Plugin のアンインストール

Plugin は、下記のようにアンインストールできます。
```
# tanzu plugin uninstall tuna-jp -y
[i] Uninstalling plugin 'tuna-jp' for target 'global'
[ok] successfully uninstalled plugin 'tuna-jp'
```

# 7. プラグインのビルドとパブリッシュ

下記のコマンドを実行すると、開発環境のアーキテクチャ以外のパッケージもビルドできて、ローカルの Docker にコンテナとしてプッシュされます。
```
$ export GOPATH=~/go
$ make plugin-build
$ make plugin-publish-packages
```

ファイルは下記のように生成されます。
```
$ ls artifacts/packages/*/*/global/tuna-jp/v0.0.1/*
artifacts/packages/darwin/amd64/global/tuna-jp/v0.0.1/tuna-jp-darwin_amd64.tar
artifacts/packages/darwin/arm64/global/tuna-jp/v0.0.1/tuna-jp-darwin_arm64.tar
artifacts/packages/linux/amd64/global/tuna-jp/v0.0.1/tuna-jp-linux_amd64.tar
artifacts/packages/linux/arm64/global/tuna-jp/v0.0.1/tuna-jp-linux_arm64.tar
artifacts/packages/windows/amd64/global/tuna-jp/v0.0.1/tuna-jp-windows_amd64.tar
```
```
$ ls artifacts/plugins/*/*/global/tuna-jp/v0.0.1/*
artifacts/plugins/darwin/amd64/global/tuna-jp/v0.0.1/tanzu-tuna-jp-darwin_amd64
artifacts/plugins/darwin/arm64/global/tuna-jp/v0.0.1/tanzu-tuna-jp-darwin_arm64
artifacts/plugins/linux/amd64/global/tuna-jp/v0.0.1/tanzu-tuna-jp-linux_amd64
artifacts/plugins/linux/arm64/global/tuna-jp/v0.0.1/tanzu-tuna-jp-linux_arm64
artifacts/plugins/windows/amd64/global/tuna-jp/v0.0.1/tanzu-tuna-jp-windows_amd64.exe

artifacts/plugins/darwin/amd64/global/tuna-jp/v0.0.1/test:
tanzu-tuna-jp-test-darwin_amd64

artifacts/plugins/darwin/arm64/global/tuna-jp/v0.0.1/test:
tanzu-tuna-jp-test-darwin_arm64

artifacts/plugins/linux/amd64/global/tuna-jp/v0.0.1/test:
tanzu-tuna-jp-test-linux_amd64

artifacts/plugins/linux/arm64/global/tuna-jp/v0.0.1/test:
tanzu-tuna-jp-test-linux_arm64

artifacts/plugins/windows/amd64/global/tuna-jp/v0.0.1/test:
tanzu-tuna-jp-test-windows_amd64.exe
```

コンテナ レジストリに登録したリポジトリからのインストールはまだ謎なので、いずれ読み解ければ・・・

# おまけ：とりあえず試してみる

```
$ curl -OL https://github.com/vmware-tanzu/tanzu-cli/releases/download/v1.1.0/tanzu-cli-linux-amd64.tar.gz
$ tar zxf tanzu-cli-linux-amd64.tar.gz
$ mkdir ~/bin
$ export PATH=~/bin:$PATH
$ install v1.1.0/tanzu-cli-linux_amd64 ~/bin/tanzu
$ curl -OL https://github.com/gowatana/tanzu-cli-tunajp/releases/download/v0.0.1/plugin_bundle.tar.gz
$ mkdir tuna-jp
$ tar zxf plugin_bundle.tar.gz -C tuna-jp
$ tanzu config eula accept
$ export TANZU_CLI_CEIP_OPT_IN_PROMPT_ANSWER=no
$ tanzu plugin install all --local-source tuna-jp/linux/amd64
$ tanzu tuna-jp get
$ tanzu tuna-jp janken --hand
```

以上、Tanzu CLI の Plugin を作成してみる話でした。