diff --git a/itamae.gemspec b/itamae.gemspec index fa8f93a6..4c84ee34 100644 --- a/itamae.gemspec +++ b/itamae.gemspec @@ -30,4 +30,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "pry-byebug" spec.add_development_dependency "docker-api", "~> 1.20" spec.add_development_dependency "fakefs" + spec.add_development_dependency "fluent-logger" end diff --git a/lib/itamae.rb b/lib/itamae.rb index 889201c4..ff143137 100644 --- a/lib/itamae.rb +++ b/lib/itamae.rb @@ -3,6 +3,8 @@ require "itamae/cli" require "itamae/recipe" require "itamae/resource" +require "itamae/handler" +require "itamae/handler_proxy" require "itamae/recipe_children" require "itamae/logger" require "itamae/node" diff --git a/lib/itamae/handler.rb b/lib/itamae/handler.rb new file mode 100644 index 00000000..43f30a32 --- /dev/null +++ b/lib/itamae/handler.rb @@ -0,0 +1,21 @@ +require 'itamae/handler/base' + +module Itamae + module Handler + def self.from_type(type) + first_time = true + + class_name = type.split('_').map(&:capitalize).join + self.const_get(class_name) + rescue NameError + require "itamae/handler/#{type}" + + if first_time + first_time = false + retry + else + raise + end + end + end +end diff --git a/lib/itamae/handler/base.rb b/lib/itamae/handler/base.rb new file mode 100644 index 00000000..86c9c868 --- /dev/null +++ b/lib/itamae/handler/base.rb @@ -0,0 +1,40 @@ +require 'socket' + +module Itamae + module Handler + class Base + attr_reader :recipes, :resources, :actions + + def initialize(options) + @options = options + + @recipes = [] + @resources = [] + @actions = [] + end + + def event(type, payload = {}) + case type + when :recipe_started + @recipes << payload + when :recipe_completed, :recipe_failed + @recipes.pop + when :resource_started + @resources << payload + when :resource_completed, :resource_failed + @resources.pop + when :action_started + @actions << payload + when :action_completed, :action_failed + @actions.pop + end + end + + private + + def hostname + @hostname ||= @options['hostname'] || Socket.gethostname + end + end + end +end diff --git a/lib/itamae/handler/debug.rb b/lib/itamae/handler/debug.rb new file mode 100644 index 00000000..af7f289c --- /dev/null +++ b/lib/itamae/handler/debug.rb @@ -0,0 +1,10 @@ +module Itamae + module Handler + class Debug < Base + def event(type, payload = {}) + super + Itamae.logger.info("EVENT:#{type} #{payload}") + end + end + end +end diff --git a/lib/itamae/handler/fluentd.rb b/lib/itamae/handler/fluentd.rb new file mode 100644 index 00000000..5d78b08c --- /dev/null +++ b/lib/itamae/handler/fluentd.rb @@ -0,0 +1,44 @@ +module Itamae + module Handler + class Fluentd < Base + attr_accessor :fluent_logger # for test + + def initialize(*) + super + load_fluent_logger + end + + def event(type, payload = {}) + super + + unless @fluent_logger.post(type, payload.merge(hostname: hostname)) + Itamae.logger.warn "Sending logs to Fluentd failed: #{@fluent_logger.last_error}" + end + end + + private + + def load_fluent_logger + begin + require 'fluent-logger' + rescue LoadError + raise "Loading fluent-logger gem failed. Please install 'fluent-logger' gem to use fluentd handler." + end + + @fluent_logger = Fluent::Logger::FluentLogger.new(tag_prefix, host: fluentd_host, port: fluentd_port) + end + + def tag_prefix + @options['tag_prefix'] || 'itamae_server' + end + + def fluentd_host + @options['host'] || 'localhost' + end + + def fluentd_port + (@options['port'] || 24224).to_i + end + end + end +end diff --git a/lib/itamae/handler/json.rb b/lib/itamae/handler/json.rb new file mode 100644 index 00000000..bbe6fbd4 --- /dev/null +++ b/lib/itamae/handler/json.rb @@ -0,0 +1,22 @@ +module Itamae + module Handler + class Json < Base + def initialize(*) + super + require 'time' + open_file + end + + def event(type, payload = {}) + super + @f.puts({'time' => Time.now.iso8601, 'event' => type, 'payload' => payload}.to_json) + end + + private + + def open_file + @f = open(@options.fetch('path'), 'a') + end + end + end +end diff --git a/lib/itamae/handler_proxy.rb b/lib/itamae/handler_proxy.rb new file mode 100644 index 00000000..c8c206be --- /dev/null +++ b/lib/itamae/handler_proxy.rb @@ -0,0 +1,38 @@ +module Itamae + class HandlerProxy + def initialize + @instances = [] + end + + def register_instance(instance) + @instances << instance + end + + def event(*args, &block) + if block_given? + _event_with_block(*args, &block) + else + _event(*args) + end + end + + private + + def _event(*args) + @instances.each do |i| + i.event(*args) + end + end + + def _event_with_block(event_name, *args, &block) + event("#{event_name}_started".to_sym, *args) + block.call + rescue + event("#{event_name}_failed".to_sym, *args) + raise + else + event("#{event_name}_completed".to_sym, *args) + end + end +end + diff --git a/lib/itamae/recipe.rb b/lib/itamae/recipe.rb index e923fa24..68eafce0 100644 --- a/lib/itamae/recipe.rb +++ b/lib/itamae/recipe.rb @@ -61,9 +61,11 @@ def load(vars = {}) def run show_banner - Itamae.logger.with_indent do - @children.run - run_delayed_notifications + @runner.handler.event(:recipe, path: @path) do + Itamae.logger.with_indent do + @children.run + run_delayed_notifications + end end end diff --git a/lib/itamae/resource/base.rb b/lib/itamae/resource/base.rb index 36ce7faa..f3d3400f 100644 --- a/lib/itamae/resource/base.rb +++ b/lib/itamae/resource/base.rb @@ -121,26 +121,31 @@ def initialize(recipe, resource_name, &block) end def run(specific_action = nil) - Itamae.logger.debug "#{resource_type}[#{resource_name}]" - - Itamae.logger.with_indent_if(Itamae.logger.debug?) do - if do_not_run_because_of_only_if? - Itamae.logger.debug "#{resource_type}[#{resource_name}] Execution skipped because of only_if attribute" - return - elsif do_not_run_because_of_not_if? - Itamae.logger.debug "#{resource_type}[#{resource_name}] Execution skipped because of not_if attribute" - return - end + runner.handler.event(:resource, resource_type: resource_type, resource_name: resource_name) do + Itamae.logger.debug "#{resource_type}[#{resource_name}]" + + Itamae.logger.with_indent_if(Itamae.logger.debug?) do + if do_not_run_because_of_only_if? + Itamae.logger.debug "#{resource_type}[#{resource_name}] Execution skipped because of only_if attribute" + return + elsif do_not_run_because_of_not_if? + Itamae.logger.debug "#{resource_type}[#{resource_name}] Execution skipped because of not_if attribute" + return + end + + [specific_action || attributes.action].flatten.each do |action| + run_action(action) + end - [specific_action || attributes.action].flatten.each do |action| - run_action(action) + verify unless runner.dry_run? + if updated? + notify + runner.handler.event(:resource_updated) + end end - verify unless runner.dry_run? - notify if updated? + @updated = false end - - @updated = false rescue Backend::CommandExecutionError Itamae.logger.error "#{resource_type}[#{resource_name}] Failed." exit 2 @@ -151,15 +156,7 @@ def action_nothing end def resource_type - humps = [] - self.class.name.split("::").last.each_char do |c| - if "A" <= c && c <= "Z" - humps << c.downcase - else - humps.last << c - end - end - humps.join('_') + self.class.name.split("::").last.scan(/[A-Z][^A-Z]+/).map(&:downcase).join('_') end private @@ -167,45 +164,50 @@ def resource_type alias_method :current, :current_attributes def run_action(action) - original_attributes = @attributes # preserve and restore later - @current_action = action + runner.handler.event(:action, action: action) do + original_attributes = @attributes # preserve and restore later + @current_action = action - clear_current_attributes + clear_current_attributes - Itamae.logger.debug "#{resource_type}[#{resource_name}] action: #{action}" + Itamae.logger.debug "#{resource_type}[#{resource_name}] action: #{action}" - return if action == :nothing + return if action == :nothing - Itamae.logger.with_indent_if(Itamae.logger.debug?) do - Itamae.logger.debug "(in pre_action)" - pre_action + Itamae.logger.with_indent_if(Itamae.logger.debug?) do + Itamae.logger.debug "(in pre_action)" + pre_action - Itamae.logger.debug "(in set_current_attributes)" - set_current_attributes + Itamae.logger.debug "(in set_current_attributes)" + set_current_attributes - Itamae.logger.debug "(in show_differences)" - show_differences + Itamae.logger.debug "(in show_differences)" + show_differences - method_name = "action_#{action}" - if runner.dry_run? - unless respond_to?(method_name) - Itamae.logger.error "action #{action.inspect} is unavailable" - end - else - args = [method_name] - if method(method_name).arity == 1 - # for plugin compatibility - args << runner.options + method_name = "action_#{action}" + if runner.dry_run? + unless respond_to?(method_name) + Itamae.logger.error "action #{action.inspect} is unavailable" + end + else + args = [method_name] + if method(method_name).arity == 1 + # for plugin compatibility + args << runner.options + end + + public_send(*args) end - public_send(*args) + if different? + updated! + runner.handler.event(:attribute_changed, from: @current_attributes, to: @attributes) + end end - updated! if different? + @current_action = nil + @attributes = original_attributes end - - @current_action = nil - @attributes = original_attributes end def clear_current_attributes diff --git a/lib/itamae/resource/file.rb b/lib/itamae/resource/file.rb index 69e0ad7d..9fc253f4 100644 --- a/lib/itamae/resource/file.rb +++ b/lib/itamae/resource/file.rb @@ -50,8 +50,8 @@ def show_differences super - if current.exist && @temppath - show_file_diff + if @temppath + compare_file end end @@ -113,8 +113,14 @@ def action_edit(options) private - def show_file_diff - diff = run_command(["diff", "-u", attributes.path, @temppath], error: false) + def compare_file + compare_to = if current.exist + attributes.path + else + '/dev/null' + end + + diff = run_command(["diff", "-u", compare_to, @temppath], error: false) if diff.exit_status == 0 # no change Itamae.logger.debug "file content will not change" @@ -132,6 +138,7 @@ def show_file_diff Itamae.logger.info line.chomp end end + runner.handler.event(:file_content_changed, diff: diff.stdout) end end diff --git a/lib/itamae/runner.rb b/lib/itamae/runner.rb index d3aa2858..136aa268 100644 --- a/lib/itamae/runner.rb +++ b/lib/itamae/runner.rb @@ -20,11 +20,14 @@ def run(recipe_files, backend_type, options) attr_reader :node attr_reader :tmpdir attr_reader :children + attr_reader :handler def initialize(backend, options) @backend = backend @options = options + prepare_handler + @node = create_node @tmpdir = "/tmp/itamae_tmp" @children = RecipeChildren.new @@ -106,5 +109,16 @@ def create_node Node.new(hash, @backend) end + + def prepare_handler + @handler = HandlerProxy.new + (@options[:handlers] || []).each do |handler| + type = handler.delete('type') + unless type + raise "#{type} field is not set" + end + @handler.register_instance(Handler.from_type(type).new(handler)) + end + end end end diff --git a/spec/unit/lib/itamae/handler/base_spec.rb b/spec/unit/lib/itamae/handler/base_spec.rb new file mode 100644 index 00000000..e24e4192 --- /dev/null +++ b/spec/unit/lib/itamae/handler/base_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Itamae::Handler::Base do + subject(:handler) { described_class.new({}) } + + context "when receiving recipe_started event" do + it "stores the payload" do + subject.event(:recipe_started, :payload) + expect(subject.recipes).to eq([:payload]) + end + end + + context "when receiving recipe_completed event" do + before do + subject.event(:recipe_started, :payload) + end + + it "pops the payload" do + subject.event(:recipe_completed, :payload) + expect(subject.recipes).to eq([]) + end + end + + context "when receiving recipe_failed event" do + before do + subject.event(:recipe_started, :payload) + end + + it "pops the payload" do + subject.event(:recipe_failed, :payload) + expect(subject.recipes).to eq([]) + end + end +end diff --git a/spec/unit/lib/itamae/handler/fluentd_spec.rb b/spec/unit/lib/itamae/handler/fluentd_spec.rb new file mode 100644 index 00000000..9b2a4a0f --- /dev/null +++ b/spec/unit/lib/itamae/handler/fluentd_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' +require 'itamae/handler/fluentd' + +describe Itamae::Handler::Fluentd do + subject(:handler) do + described_class.new(options).tap do |h| + h.fluent_logger = fluent_logger + end + end + let(:options) { {'hostname' => 'me'} } + let(:fluent_logger) { Fluent::Logger::TestLogger.new } + + describe '#event' do + it 'posts a record to fluent logger' do + subject.event(:type, {arg: 'value'}) + expect(fluent_logger.queue).to eq([{arg: 'value', hostname: 'me'}]) + end + end +end diff --git a/spec/unit/lib/itamae/handler_proxy_spec.rb b/spec/unit/lib/itamae/handler_proxy_spec.rb new file mode 100644 index 00000000..48dfcef3 --- /dev/null +++ b/spec/unit/lib/itamae/handler_proxy_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +module Itamae + describe HandlerProxy do + let(:handler) { instance_double(Handler::Base) } + before { subject.register_instance(handler) } + + describe "#event" do + context "with block" do + context "when the block completes" do + it "fires *_started and *_completed events" do + expect(handler).to receive(:event).with(:name_started, :arg) + expect(handler).to receive(:event).with(:name_completed, :arg) + subject.event(:name, :arg) { } + end + end + + context "when the block fails" do + it "fires *_started and *_failed events" do + expect(handler).to receive(:event).with(:name_started, :arg) + expect(handler).to receive(:event).with(:name_failed, :arg) + expect { + subject.event(:name, :arg) { raise } + }.to raise_error + end + end + end + + context "without block" do + it "fires the event" do + expect(handler).to receive(:event).with(:name, :arg) + subject.event(:name, :arg) + end + end + end + end +end + diff --git a/spec/unit/lib/itamae/handler_spec.rb b/spec/unit/lib/itamae/handler_spec.rb new file mode 100644 index 00000000..830c3d40 --- /dev/null +++ b/spec/unit/lib/itamae/handler_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +module Itamae + describe Handler do + describe ".from_type" do + it "returns handler class" do + expect(described_class.from_type('debug')).to eq(Handler::Debug) + end + end + end +end diff --git a/spec/unit/lib/itamae/resource/base_spec.rb b/spec/unit/lib/itamae/resource/base_spec.rb index 65da8851..8da23229 100644 --- a/spec/unit/lib/itamae/resource/base_spec.rb +++ b/spec/unit/lib/itamae/resource/base_spec.rb @@ -100,14 +100,15 @@ class TestResource < Itamae::Resource::Base describe TestResource do subject(:resource) { described_class.new(recipe, "name") } - let(:commands) { double(:commands) } + let(:handler) { Itamae::HandlerProxy.new } let(:runner) do instance_double(Itamae::Runner).tap do |r| allow(r).to receive(:dry_run?).and_return(false) + allow(r).to receive(:handler).and_return(handler) end end let(:recipe) do - double(:recipe).tap do |r| + instance_double(Itamae::Recipe).tap do |r| allow(r).to receive(:runner).and_return(runner) end end