Ruby
JSON
chef
inspec

ChefからInSpecにattributeをJSON経由で渡したら、シンボルが文字列になっていた

問題

ChefレシピからattibuteをJSONでテストに渡していたら、symbolを想定していた変数がstringとして設定されていた。

考えてみれば当たり前だが、JSONの書式にはsymbolに相当するものが無い。
JSON.pretty_generateなどでJSONに変換した時点でstringに置き換えられていた。

recipes/default.rb
node.default['service']['foo']['action'] = [:start, :enable]

...

require 'json'
IO.write('/tmp/node.json', JSON.pretty_generate(node.to_hash))

対応1: 文字列の前提でテストを作る

シンプルに比較対象をシンボルから文字列にする。。。
まあchefのattributeでsymbolが必要なのはactionとnotifies, subscribes くらいの様な気がするので、主にそこだけ気をつければ良いのだが。。。。

hash['service'].each do |s_name, s_attr|
  describe service(s_name) do
    it { should be_enabled }     if s_attr['action'].include?('enable')
    it { should_not be_enabled } if s_attr['action'].include?('disable')
    it { should be_running }     if s_attr['action'].include?('start')
    it { should_not be_running } if s_attr['action'].include?('stop')
  end
end

対応2: symbolのまま渡す方法は無いか。。

レシピ側で、.inspectメソッドでハッシュを文字列として書き出し、

recipes/default.rb
...

hash = node.to_hash
hash['run_list'] = node['run_list'] # ここだけ読み込み時のエラー回避の為に置き換え
IO.write('/tmp/node.hash', hash.inspect)

テストシナリオではevalで読み込ませれば、うまく行きそうに思えた。。。。が、

test/integration/default/default_test.rb
node = {}
eval('node = ' + `cat /tmp/node.hash`)

...

残念

試してみると、Chefはリモート、InSpecはローカルで実行されていて、InSpecのテストから直接はリモートのファイルを参照できなかった。

shell veriferにして $KITCHEN_USERNAME などを使うか、テストスクリプト中で ssh などを行えばできるのだろうが、、、
その様なことを考えなくて良いjson( ).params によるjson渡しが楽すぎて、シンボルをそのまま渡すことはとりあえず断念。

追記:kitchen exec を用いることで1ノードなら対応できた

test/integration/default/default_test.rb
node = {}
eval('node = ' + ` kitchen exec --command='cat /tmp/node.hash' | tail +2 `)

...

kitchenのインスタンス名は渡せていないので、対象が1システムの場合のみ。
やはりshell veriferで素直にローカルにscpかな。。

実はjson( ).paramsはすごかった

InSpecのテストで、jsonからパラメーターを読み込むのに、何気に使っていたjson( ).paramsは、テスト実行ノードがたとえリモートでも、そこのファイルを読み込んでくれる native inspec resource の様子。

test/integration/default/default_test.rb
node = json('/tmp/node.json').params

...

require 'json' も不要

対応3: レシピでノードオブジェクトのvalueとしてsymbol(もしくはsymbolのarray)は使わない様に統一

レシピでactionに渡す値を最初から.to_symや.mapを使って加工すれば良いのでしょうが、。。。本末転倒な感じ。

参照