ようへいの日々精進XP

よかろうもん

ステイホームなゴールデンウィークなので Vagrant で goss を実行出来るプラグインを作ってみた

tl;dr

皆さん, ステイホームしてますか.

ちょっと前に久しぶりに EC2 のカスタム AMI を作成するにあたって, Vagrant + vagrant-ec2 + mitamae + goss という組み合わせで実装したりテストをしていました. mitamae でレシピを流して, goss でテストをするという流れです.

github.com

github.com

mitamae も goss も VagrantShell Provisioner で以下のように実行することが出来るのですが, 以下のように goss だけ走らせたい時に面倒だなあという思いがあり Vagrant Plugin 実装の勉強も兼ねて Provisioner プラグインを作ってみました.

$ vagrant provision --provision-with=goss

vagrant-plugin-goss

リポジトリ

github.com

Vagrantfile

goss は Golang で書かれている為, ワンバイナリ化されています. これをダウンロードする為に wget コマンドは必須です.

Vagrant.configure('2') do |config|
  config.vm.box = 'centos/7'

  config.vm.provider "virtualbox" do |vb|
     vb.memory = '512'
  end  

  config.vm.synced_folder '.', '/vagrant',
    disabled: false,
    type: 'rsync',
    rsync__exclude: [
      '.envrc',
      '.ruby-version',
      '.env',
      './tmp/',
      './bk/'
    ]
  config.vm.provision :shell, inline: <<~BASH
    sudo yum install -y wget httpd
  BASH

  config.vm.provision :goss do |goss|
    goss.root_path = '/vagrant'
    # root_path からの絶対パスで指定する #{root_path}/foo/bar であれば, /foo/bar と指定
    goss.spec_file = '/spec/goss.yaml'
    # root_path からの絶対パスで指定する #{root_path}/baz であれば, /baz と指定
    # goss.vars_file = '/vars.yaml'
    # root_path からの絶対パスで指定する #{root_path}/foo/bar であれば, /foo/bar と指定
    goss.goss_path = '/goss'
    # goss の output format を指定する, デフォルトは documentation, junit, json, nagios, rspecish, tap, silent
    # goss.output_format = 'junit'
  end
end

後は, 任意のディレクトリに goss が利用する YAML ファイル (--gossfile オプションで指定するファイル) を用意します.

$ mkdir spec
$ cat << EOF > ./spec/goss.yaml
package:
  httpd:
    installed: true
  wget:
    installed: true
EOF

後は, vagrant up からの vagrant provisionvagrant rsync からの vagrant provision 等, 素敵な vagrant 生活をお送り下さい.

f:id:inokara:20200504164943g:plain

vagrant-plugin-goss 前夜

ちなみに, vagrant-plugin-goss を作る前は, 以下のように Shell Provisioner を書いておりました.

  config.vm.provision :shell, inline: <<~BASH
    cd /vagrant && \
    ./bin/depend.sh && \
    ./bin/mitamae local bootstrap.rb --node-yaml=node.yml #{args}
    ./bin/goss --gossfile spec/goss.yaml --vars node.yml validate --format documentation 
  BASH

この状態で mitamae だけ, goss だけ実行したい場合, いちいち該当行以外をコメントアウトしていました.

Vagrant Plugin の実装について

以下は

このプラグインを作っていく上で学んだことなどを書きます. あくまでも自分の主観で書いているので, 用語の使い方等に誤りがあるかもしれませんがご容赦下さい.

参考

以下のドキュメントや Github リポジトリを参考にしました.

github.com

github.com

www.vagrantup.com

www.vagrantup.com

www.vagrantup.com

plugin.rb が起点になる

以下は lib/vagrant-goss/plugin.rb の抜粋です.

module VagrantPlugins
  module Goss
    class Plugin < Vagrant.plugin('2')
      name 'goss'
      description <<-DESC
      This plugin executes a goss suite against a running Vagrant instance.
      DESC

      config(:goss, :provisioner) do
        require_relative 'config'
        Config
      end

      provisioner(:goss) do
        require_relative 'provisioner'
        Provisioner
      end
    end
  end
end

プラグインにとって, ここが起点になり, 今回は Provisioner プラグインとなるので, configprovisioner のブロックを定義します. 個々のブロックで require_relative の引数に指定している configprovisioner をコツコツと書いていくことになります.

ちなみに configprovisioner 以外にも command 等も用意されています. commandについては,vagrant xxxx` と実行する際に実行されるコマンド等を定義出来るのかなーと予想しています.

config.rb

以下は lib/vagrant-goss/config.rb の抜粋です.

module VagrantPlugins
  module Goss
    class Config < Vagrant.plugin('2', :config)
      attr_accessor :output_format
      attr_accessor :spec_file
... 略 ...
      def initialize
        super
        @output_format = UNSET_VALUE
        @spec_file = UNSET_VALUE
... 略 ...
      def finalize!
        @output_format = 'documentation' if @output_format == UNSET_VALUE
        @spec_file = 'goss.yaml' if @spec_file == UNSET_VALUE
 ... 略 ...

attr_accessor しているのは, Vagrantfile で以下のように (goss.root_path とか) 書くためですよね. (多分

  config.vm.provision :goss do |goss|
    goss.root_path = '/vagrant'
    # root_path からの絶対パスで指定する #{root_path}/foo/bar であれば, /foo/bar と指定
    goss.spec_file = '/spec/goss.yaml'
... 略 ...

provisioner.rb

以下は lib/vagrant-goss/provisioner.rb の抜粋です.

... 略 ...
module VagrantPlugins
  module Goss
    class Provisioner < Vagrant.plugin('2', :provisioner)
... 略 ...
      def provision
        vars_option = "--vars #{@vars_file} " unless @vars_file.nil?
        run = "#{@goss_path} --gossfile #{@spec_file} " +
              "#{vars_option} " +
              "validate " +
              "--format #{@output_format} --color" 
        run_command(run) if download_goss(@goss_path)
      end 

goss 実行時の出力を Vagrant 標準の出力 (以下のように, 標準出力の場合には緑で表示されたり, default: が行頭につくようにする) にする為に試行錯誤しました.

f:id:inokara:20200504163530p:plain

結局, Vagrant Shell Provisioner のソースコードを参考 (そのまま利用) させて頂きました. 以下の部分です.

      # refer to: https://github.com/hashicorp/vagrant/blob/master/plugins/provisioners/shell/provisioner.rb#L67-L81
      def handle_comm(type, data)
        if [:stderr, :stdout].include?(type)
          # Output the data with the proper color based on the stream.
          color = type == :stdout ? :green : :red

          # Clear out the newline since we add one
          data = data.chomp
          return if data.empty?

          options = {}
          options[:color] = color if !config.keep_color

          machine.ui.detail(data.chomp, options)
        end
      end

ドキュメントにも書かれていました.

Most plugins are likely going to want to do some sort of input/output. Plugins should never use Ruby's built-in puts or gets style methods. Instead, all input/output should go through some sort of Vagrant UI object. The Vagrant UI object properly handles cases where there is no TTY, output pipes are closed, there is no input pipe, etc.

  • puts 等の Ruby 標準の入出力メソッドは使ってはいけない
  • 代わりに Vagrant が提供している UI オブジェクトを利用する必要がある
  • UI オブジェクトは ui プロパティを介して, すべての Vagrant::Environment で使用することが出来る

以上

ステイホームで素敵なインフラストラクチャアズコードな生活をお送り下さい.