Skip to content

Instantly share code, notes, and snippets.

@hiroakiukaji
Last active December 22, 2015 02:26
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save hiroakiukaji/6ba1dda2c9eb7bddd3d0 to your computer and use it in GitHub Desktop.
世界一簡単な cf CLI Plugin Hands-on

世界一簡単な cf CLI Plugin Hands-on

この記事は、 Open PaaS Advent Calendar 2015 の22日目の記事です。
今回はオープンソースの PaaS として有名な Cloud Foundry の CLI Plugin の作り方を、初心者の方にも試しやすいハンズオン形式で書いてみようと思います。
基本的に内容を追っていけばPluginのプロトタイプを作ることができるようになっているはずなので、そういえば Plugin の存在は知っているけど書いたことはないなぁ、なんて方は是非試してみてください。

まず cf CLI Pluginって何?

Cloud Foundryを使ったことがある人ならご存知の、 cf CLI コマンド。
cf CLI Plugin は、既存の cf コマンドを活用したり独自のロジックを組み込んだりし、ユーザが独自にCLIコマンドを開発できる機能です。

早速Pluginを書く・・・その前に環境の設定から

Cloud Foundry 環境

さて、本来ならば 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

のような環境で作業をしています。

Golang 環境

今回の環境では 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

以下の作業はこの設定を前提に記述してあります。

その1. 最小構成の Plugin

さて、それでは早速 Plugin の作成に移りましょう。
作業は適当なディレクトリ配下で行います。

$ mkdir cf-plugin
$ cd cf-plugin

先ほども触れましたが、 Plugin のコードは Go 言語で書かれます。
特別な作業をしないのであれば、書くべきソースコードの実体は1つの *.go ファイル1つで済んでしまいます。

ちなみに全体の手順としては、

  1. 頑張って Plugin 本体のコードを書く
  2. Build して binary ファイルを作る
  3. Cloud Foundry に Plugin をインストールする

これだけです。

頑張って 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! を使うのは世の常です。
まずはこれを動かすところから始めてみましょう。

Build して binary ファイルを作る

さて、実はこれを 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 ができているはずです。

Cloud Foundry に 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 を作って実行するところまではできるようになりました。

その2. Plugin から cf コマンドを呼び出してみる

さて、では次に、既存の 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 が行われました。

その3. ちょっとだけ工夫を加えてみる

流石にこれだけだと物足りないので (というか Plugin 化する意味すら疑わしいレベルなので) 、もう少しだけ工夫を加えた Plugin を作ってみます。
とは言いつつも記事のタイトル的にあまり凝ったものを作るわけにもいかないので・・・ cf push 時に現在の git の branch をチェックする機能でも作ってみましょうか。

Plugin の基本的な機能は、

  1. デプロイを行う
  2. ( git を使っている場合)今の branch はこれだけどOKか?と質問してくる
  3. 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 が人々の目に止まれば一躍有名になれるかもしれません。
皆さんも是非チャレンジしてみてください。

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment