itamae resourceの書き方

[itamae](https://github.com/ryotarai/itamae)は軽量でChefライクなサーバ構成管理ツールです。詳しくは[こちら](https://speakerdeck.com/ryotarai/itamae-infra-as-code-xian-zhuang-que-ren-hui)。
本稿ではitamaeで独自のresourceを定義する方法を紹介します。

## resourceとは
文字通りサーバが持つ各リソースの種類に対応していて、itamaeではレシピにresourceを列挙することによってサーバの状態を定義します。
itamaeにはresource pluginを定義するための機構があり、簡単に独自のresourceを定義することができます。

## plugin機構の仕組み
### レシピの`method_missing`で`Itamae::Plugin::Resource::*`が探される
レシピに

```rb
foo "bar" do
  # ...
end
```

と書くと、[レシピの`method_missing`](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/recipe.rb#L68-L80)で[`Itamae::Plugin::Resource::Foo`が探されます](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/resource.rb#L28)。
itamaeのレシピが読み込まれた段階でこのFoo resourceがレシピの`children`に追加されます。

そのため、`bundle gem`の自動生成規則に従うと、pluginのgem名は`itamae-plugin-resource-foo`にする必要があります。

### レシピに追加されたresourceがrunされる
レシピの読み込みが終わると、レシピに追加された各resourceがrunされます。 [★](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/recipe_children.rb#L54)
なので、

```rb
resource = Itamae::Plugin::Resource::Foo.new(recipe, "bar", &block)
resource.run(nil, dry_run: options[:dry_run])
```

が実行されたらサーバの状態に応じた適切なコマンドが走るように`Itamae::Plugin::Resource::Foo`を定義すればよいことになります。
resourceに共通して必要なメソッドが`Itamae::Resource::Base`に定義されているので、新たにresourceを定義するときはこれを継承し、必要な部分をオーバーライドする必要があります。

## `Itamae::Resource::Base`の仕組み
実際に定義するとなると[itamae/resource/base.rb](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/resource/base.rb)をちゃんと読む必要がありますが、ここでは概要だけ説明します。

- `#new`でresourceにaction等のattributeがセットされる [★](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/resource/base.rb#L107)
  - attributeは、ブロックの内側で使えるメソッドで、これがリソースの状態になる
  - `define_attribute`を列挙することで任意のパラメータを追加できる
- `#run`でattributeに応じたコマンドを実行する
  - `pre_action`, `set_current_attributes`, `show_differences`が順番に呼ばれる [★](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/resource/base.rb#L164-L170)
  - :action attributeに応じた`action_#{action}`が呼ばれる [★](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/resource/base.rb#L172-L174)

従って、以下の3つの変更を行えばよいことになります。

- `define_attribute`の列挙
- `pre_action`, `set_current_attributes`, `show_differences`のオーバーライド
- `action_#{action}`メソッドの作成

## 作成例: ghq resource
例として、[ghq](https://github.com/motemen/ghq)を使ってgoの流儀に従ったリポジトリ配置を行うためのリソースを作成しました。
主に[git resource](https://github.com/ryotarai/itamae/blob/21499fc916430b6cd15ccd42257f848d73528932/lib/itamae/resource/git.rb)と[mercurial resource](https://github.com/eagletmt/itamae-plugin-resource-mercurial/blob/master/lib/itamae/plugin/resource/mercurial.rb)を参考に作りました。

[k0kubun/itamae-plugin-resource-ghq](https://github.com/k0kubun/itamae-plugin-resource-ghq)

```rb
require "itamae/resource/base"

module Itamae
  module Plugin
    module Resource
      class Ghq < Itamae::Resource::Base
        define_attribute :action, default: :create
        define_attribute :repository, type: String, default_name: true

        def set_current_attributes
          ensure_ghq_availability

          result = run_command("ghq list | grep '#{attributes.repository}$'", error: false)
          current.exist = result.exit_status == 0
        end

        def action_create(options)
          unless current.exist
            run_command(["ghq", "get", attributes.repository])
          end
        end

        def action_update(options)
          result = run_command(["ghq", "get", "-u", attributes.repository])
          unless result.stdout.match(/is up to date/)
            updated!
          end
        end

        private

        def ensure_ghq_availability
          if run_command("which ghq", error: false).exit_status != 0
            raise "`ghq` command is not available. Please install ghq."
          end
        end
      end
    end
  end
end
```

すると、以下のように使えるようになります。

```rb
require "itamae/plugin/resource/ghq"

# ghq get ryotarai/itamae
ghq "ryotarai/itamae"

# ghq get -u serverspec/specinfra
ghq "serverspec/specinfra" do
  action :update
end
```