この記事は、 Open PaaS Advent Calendar 2015 の22日目の記事です。
今回はオープンソースの PaaS として有名な Cloud Foundry の CLI Plugin の作り方を、初心者の方にも試しやすいハンズオン形式で書いてみようと思います。
基本的に内容を追っていけばPluginのプロトタイプを作ることができるようになっているはずなので、そういえば Plugin の存在は知っているけど書いたことはないなぁ、なんて方は是非試してみてください。
Cloud Foundryを使ったことがある人ならご存知の、 cf CLI
コマンド。
cf CLI Plugin
は、既存の cf
コマンドを活用したり独自のロジックを組み込んだりし、ユーザが独自にCLIコマンドを開発できる機能です。
さて、本来ならば Plugin を動かすための Cloud Foundry の環境作りから話を進めるべきな気もするのですが・・・ Cloud Foundry 環境の準備/構築を話しだすと記事の分量が大変なことになってしまうので、ここではデフォルトで Cloud Foundry 環境が使えるものとして話を進めさせて下さい。
ちなみに筆者は、
- MacBook Pro 上に Bosh-Lite で Cloud Foundry を構築
- BOSH 1.3153.0
- cf-v211
- cf version 6.14.0+2654a47-2015-11-18
のような環境で作業をしています。
今回の環境では Go 言語の環境を必要とするので少しだけ触れておきます。
上記の通り今回は Mac 上での作業になるので、ここに Go 言語の環境を用意しておきましょう。
とは言っても、 gvm を使うだけです。
$ brew update
$ brew install mercurial
$ bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
$ source ~/.bash_profile
これで準備は OK なので、 go1.4 でもインストールしてみましょう。
$ gvm install go1.4
$ gvm use go1.4
$ go version
go version go1.4 darwin/amd64
これだけで終わりです。
gvm は PATH も自動で通してくれるので楽ですね。
$ echo $GOPATH
/Users/hiroaki/.gvm/pkgsets/go1.4/global
以下の作業はこの設定を前提に記述してあります。
さて、それでは早速 Plugin の作成に移りましょう。
作業は適当なディレクトリ配下で行います。
$ mkdir cf-plugin
$ cd cf-plugin
先ほども触れましたが、 Plugin のコードは Go 言語で書かれます。
特別な作業をしないのであれば、書くべきソースコードの実体は1つの *.go
ファイル1つで済んでしまいます。
ちなみに全体の手順としては、
- 頑張って Plugin 本体のコードを書く
- Build して binary ファイルを作る
- Cloud Foundry に Plugin をインストールする
これだけです。
まずは本体のコードがないと何も始まらないので、そこから始めましょう。
このあたり を眺めつつ、試しに最小構成の cf CLI Plugin を書いてみると、こんな感じでしょうか。
まずは「コマンドを呼び出した際に "Hello, CF Plugin!" を返す」、ただそれだけの Plugin です。
cf-plugin$ vi plugin.go
package main
import (
"fmt"
"github.com/cloudfoundry/cli/plugin"
)
type MyFirstPlugin struct{}
func (mfp *MyFirstPlugin) Run(cliConnection plugin.CliConnection, args []string) {
fmt.Println("Hello, CF Plugin!")
}
func (mfp *MyFirstPlugin) GetMetadata() plugin.PluginMetadata {
return plugin.PluginMetadata{
Name: "MyFirstPlugin",
Version: plugin.VersionType{
Major: 0,
Minor: 0,
Build: 0,
},
Commands: []plugin.Command{
plugin.Command{
Name: "my-first-plugin",
HelpText: "This is a help text.",
UsageDetails: plugin.Usage{
Usage: "my-first-plugin\n cf my-first-plugin",
},
},
},
}
}
func main() {
plugin.Start(new(MyFirstPlugin))
}
約40行足らずのコードですが、いわゆる「お約束」部分を除くと、実際にこの Plugin の根幹を担っている処理を行っているのは、
func (mfp *MyFirstPlugin) Run(cliConnection plugin.CliConnection, args []string) {
fmt.Println("Hello, CF Plugin!")
}
ここだけですね。
行数を一番消費している GetMetadata 関数が Plugin のメタデータ(バージョンやコマンドの使い方等の説明)を設定するためだけのものだ、ということさえわかれば、もう何も怖いものはありません。
Plugin としてどうなのか、と問われれば特に有用性はないのですが、ともかくプログラミングの取っ掛かりに Hello, world! を使うのは世の常です。
まずはこれを動かすところから始めてみましょう。
さて、実はこれを cf CLI Plugin として動かすには少しだけ手順が残っています。
次は build して binary ファイルを作る、です。
まずはこのコードでは外部の Golang パッケージを import しているので、そこから片付けましょう。
$ go get github.com/cloudfoundry/cli/plugin
といってもこの一手で終了です。
この時点でビルドは通るようになっているはずですが、後々のために、これらのパッケージは Godeps などで管理しておきましょう。
$ go get github.com/kr/godep
$ godep save
$ ls
Godeps plugin.go
Godeps というディレクトリが現れたでしょうか。
これをやっておくと ./Godeps/Godeps.json
で依存関係が管理され、
cf-plugin$ cat Godeps/Godeps.json
{
"ImportPath": ".",
"GoVersion": "go1.4",
"Deps": [
{
"ImportPath": "github.com/cloudfoundry/cli/plugin",
"Comment": "v6.14.0-149-gc259ad5",
"Rev": "c259ad5d253425ce5e37aab076e49573495fd069"
}
]
}
パッケージの実体は ./Godeps/_workspace/src/
配下に入ります。
cf-plugin$ tree Godeps/_workspace/src/ -L 3
Godeps/_workspace/src/
└── github.com
└── cloudfoundry
└── cli
何が楽かというと、他の人がこの Plugin を実行する際に依存パッケージが必要になった場合でも、
$ godep get
さえ行えばインストールが全て滞り無く実行される、という手筈になっています(もちろん Go 言語の環境は必要です!)。便利ですね。
なお他人に配布・公開する時の事を考えるならば、 Godeps/_workspace
配下のファイルは .gitignore
に入れておいたほうが親切かもしれません。
cf-plugin$ vi .gitignore
Godeps/_workspace
さて、本題に戻って、 plugin.go
をビルドしましょう。
cf-plugin$ go build plugin.go
cf-plugin$ ls
Godeps plugin plugin.go
無事 binary ファイル、 plugin
ができているはずです。
では、今しがた作った plugin
を Cloud Foundry の Plugin として登録し、使ってみましょう。
cf-plugin$ ls
Godeps plugin plugin.go
登録を行うには cf コマンドを使います。
cf-plugin$ cf install-plugin plugin
**Attention: Plugins are binaries written by potentially untrusted authors. Install and use plugins at your own risk.**
Do you want to install the plugin plugin? (y or n)> y
Installing plugin ./plugin...
OK
Plugin MyFirstPlugin v0.0.0 successfully installed.
cf-plugin$ cf plugins
Listing Installed Plugins...
OK
Plugin Name Version Command Name Command Help
MyFirstPlugin N/A my-first-plugin This is a help text.
Command Nameとなっている所がPluginに割り当てられたコマンド名です。
実際に動かしてみましょう。
cf-plugin$ cf my-first-plugin
Hello, CF Plugin!
無事に動きました。
CloudFoundry の Plugin である意味は皆無な出力結果ではありますが、ともかく Plugin を作って実行するところまではできるようになりました。
さて、では次に、既存の cf コマンドを利用する Plugin を書いてみましょう。
先ほどのソースコードをそのまま使って書いてみます。
処理としては「実行時に cf push
が流れる」、という Plugin になります。
cf-plugin$ vi plugin.go
package main
import (
"fmt"
"github.com/cloudfoundry/cli/plugin"
)
type MyFirstPlugin struct{}
func (mfp *MyFirstPlugin) Run(cliConnection plugin.CliConnection, args []string) {
fmt.Println("`cf push` from Plugin")
cliConnection.CliCommand("push")
}
func (mfp *MyFirstPlugin) GetMetadata() plugin.PluginMetadata {
return plugin.PluginMetadata{
Name: "MyFirstPlugin",
Version: plugin.VersionType{
Major: 0,
Minor: 0,
Build: 0,
},
Commands: []plugin.Command{
plugin.Command{
Name: "my-first-plugin",
HelpText: "This is a help text.",
UsageDetails: plugin.Usage{
Usage: "my-first-plugin\n cf my-first-plugin",
},
},
},
}
}
func main() {
plugin.Start(new(MyFirstPlugin))
}
見ての通り、変更をしたのは Run 関数の中身だけです。
なお今回の Plugin は Run 関数の引数を使っているのでここで簡単に説明をしておくと、
- cliConnection
→ plugin.CliConnection 型。非常にざっくりな説明をすると cf コマンドを流しこむために用いられ、例えば
cf app <APP_NAME>
を流したい時にはcliConnection.CliCommand("app", "<APP_NAME>")
のように使えばOK。 - args → []string 型。Pluginコマンドを打った時の引数を string の slice で受け取る。
となります。
ともかくこいつも動かしてみましょう。
まずは build とインストールを済ませ・・・
cf-plugin$ go build plugin.go
cf-plugin$ cf uninstall-plugin MyFirstPlugin
cf-plugin$ cf install-plugin plugin
cf push
だけでデプロイが可能になっている Cloud Foundry アプリケーションのあるどこかのディレクトリで Plugin を実行してみます。
$ ls
$ touch index.html manifest.yml Staticfile
$ cat <<EOF > index.html
> Hello, Staticfile!
> EOF
$ cat <<EOF > manifest.yml
> ---
> applications:
> - name: hello-staticfile
> EOF
$ ls
Staticfile index.html manifest.yml
$ cf my-first-plugin
`cf push` from Plugin
Using manifest file /Users/hiroaki/development/cf-hello-world/staticfile/manifest.yml
Creating app hello-staticfile in org ukaji / space default as admin...
OK
Creating route hello-staticfile.bosh-lite.com...
OK
Binding hello-staticfile.bosh-lite.com to hello-staticfile...
OK
Uploading hello-staticfile...
Uploading app files from: /Users/hiroaki/development/cf-hello-world/staticfile
Uploading 266B, 2 files
Done uploading
OK
Starting app hello-staticfile in org ukaji / space default as admin...
-----> Downloaded app package (4.0K)
-------> Buildpack version 1.2.1
-----> Using root folder
-----> Copying project files into public/
-----> Setting up nginx
-----> Uploading droplet (2.5M)
1 of 1 instances running
App started
OK
App hello-staticfile was started using this command `sh boot.sh`
Showing health and status for app hello-staticfile in org ukaji / space default as admin...
OK
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: hello-staticfile.bosh-lite.com
last uploaded: Tue Dec 15 13:48:14 UTC 2015
stack: cflinuxfs2
buildpack: Static file
state since cpu memory disk details
#0 running 2015-12-15 10:48:26 PM 0.0% 3.5M of 256M 0 of 1G
無事 cf push
が行われました。
流石にこれだけだと物足りないので (というか Plugin 化する意味すら疑わしいレベルなので) 、もう少しだけ工夫を加えた Plugin を作ってみます。
とは言いつつも記事のタイトル的にあまり凝ったものを作るわけにもいかないので・・・ cf push 時に現在の git の branch をチェックする機能でも作ってみましょうか。
Plugin の基本的な機能は、
- デプロイを行う
- ( git を使っている場合)今の branch はこれだけどOKか?と質問してくる
- OK である返答をすればその時点でデプロイを開始
こんな感じで行きます。
cf-plugin$ vi plugin.go
package main
import (
"fmt"
"os/exec"
"github.com/cloudfoundry/cli/plugin"
)
type MyFirstPlugin struct{}
func (mfp *MyFirstPlugin) Run(cliConnection plugin.CliConnection, args []string) {
fmt.Println("----------------")
fmt.Println(" branch checker ")
fmt.Println("----------------")
output, _ := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").CombinedOutput()
fmt.Println("On branch " + string(output))
var yn string
fmt.Printf("Is it OK? (yes/no) :")
fmt.Scanf("%s", &yn)
if yn == "yes" {
cliConnection.CliCommand("push")
} else {
fmt.Println("Bye!")
}
}
func (mfp *MyFirstPlugin) GetMetadata() plugin.PluginMetadata {
return plugin.PluginMetadata{
Name: "MyFirstPlugin",
Version: plugin.VersionType{
Major: 0,
Minor: 0,
Build: 0,
},
Commands: []plugin.Command{
plugin.Command{
Name: "my-first-plugin",
HelpText: "This is a help text.",
UsageDetails: plugin.Usage{
Usage: "my-first-plugin\n cf my-first-plugin",
},
},
},
}
}
func main() {
plugin.Start(new(MyFirstPlugin))
}
ソースコードはこんな感じです。
上で挙げた条件分岐をそのまま Run 関数内に落としただけです。簡単ですね。
一応動作確認もしてみましょう。
cf-plugin$ go build plugin.go
cf-plugin$ cf uninstall-plugin MyFirstPlugin
cf-plugin$ cf install-plugin plugin
$ ls
Staticfile index.html manifest.yml
$ cf my-first-plugin
----------------
branch checker
----------------
On branch master
Is it OK? (yes/no) :no
Bye!
$ cf my-first-plugin
----------------
branch checker
----------------
On branch master
Is it OK? (yes/no) :yes
Using manifest file /Users/hiroaki/development/cf-hello-world/staticfile/manifest.yml
Updating app hello-staticfile in org ukaji / space default as admin...
OK
Uploading hello-staticfile...
Uploading app files from: /Users/hiroaki/development/cf-hello-world/staticfile
Uploading 266B, 2 files
Done uploading
OK
Stopping app hello-staticfile in org ukaji / space default as admin...
OK
Starting app hello-staticfile in org ukaji / space default as admin...
-----> Downloaded app package (4.0K)
-----> Downloaded app buildpack cache (4.0K)
-------> Buildpack version 1.2.1
-----> Using root folder
-----> Copying project files into public/
-----> Setting up nginx
-----> Uploading droplet (2.5M)
1 of 1 instances running
App started
OK
App hello-staticfile was started using this command `sh boot.sh`
Showing health and status for app hello-staticfile in org ukaji / space default as admin...
OK
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: hello-staticfile.bosh-lite.com
last uploaded: Tue Dec 22 01:11:04 UTC 2015
stack: cflinuxfs2
buildpack: Static file
state since cpu memory disk details
#0 running 2015-12-22 10:11:16 AM 0.0% 3.5M of 256M 0 of 1G
今回は「世界一簡単な cf CLI Plugin Hands-on」ということで、簡単に、かつ Cloud Foundry 環境さえあれば誰でも試せる内容で記事を書いてみたつもりです。
今回紹介した Plugin はプログラミング入門で言うところの Hello, World! であり初歩中の初歩な位置付けですが、工夫次第ではもっと高機能で便利な Plugin を作ることも可能です。
Cloud Foundry 自体がオープンソースであるという文化も相まって、公開した自作 Plugin が人々の目に止まれば一躍有名になれるかもしれません。
皆さんも是非チャレンジしてみてください。