class Vagrant::Action::Builder

Action builder which provides a nice DSL for building up a middleware sequence for Vagrant actions. This code is based heavily off of ‘Rack::Builder` and `ActionDispatch::MiddlewareStack` in Rack and Rails, respectively.

Usage

Building an action sequence is very easy:

app = Vagrant::Action::Builder.new.tap do |b|
  b.use MiddlewareA
  b.use MiddlewareB
end

Vagrant::Action.run(app)

Constants

MiddlewareArguments

Container for Action arguments

StackItem

Item within the stack

Attributes

primary[RW]

Action Hooks allow plugin authors to inject their code wherever they want in the action stack. The methods they get are:

- prepend/append, which puts their middleware at the beginning or end
  of the whole stack
- before/after, which attaches their middleware to an existing item
  in the stack

Applying Action Hooks properly gets tricky because the action stack becomes deeply nested with things like Action::Builtin::Call and Builder#use(other_builder). The way it breaks down is:

- prepend/append hooks should be applied only at the top level stack,
  so they run once at the beginning or end
- before/after hooks should be applied in every sub-builder, because
  they will only actually attach if they find their target sibling

We achieve this behavior by tracking if we are a “primary” Builder, and only running prepend/append operations when we are.

Note this difference only applies to action_hooks registered with machine action names and not action_hooks which reference middleware directly, which only support prepend/append and are handled in apply_dynamic_updates.

@return [Boolean] true if this is a primary / top-level Builder @see Vagrant::Action::PrimaryRunner @see Vagrant::Action::Hook

stack[R]

This is the stack of middlewares added. This should NOT be used directly.

@return [Array]

Public Class Methods

build(middleware, *args, **keywords, &block) click to toggle source

This is a shortcut for a middleware sequence with only one item in it. For a description of the arguments and the documentation, please see {#use} instead.

@return [Builder]

# File lib/vagrant/action/builder.rb, line 66
def self.build(middleware, *args, **keywords, &block)
  new.use(middleware, *args, **keywords, &block)
end
new() click to toggle source
# File lib/vagrant/action/builder.rb, line 70
def initialize
  @stack = []
  @logger = Log4r::Logger.new("vagrant::action::builder")
end

Public Instance Methods

apply_action_name(env) click to toggle source

If action hooks have not already been set, this method will perform three tasks:

1. Load any hook triggers defined for the action_name
2. Load any action_hooks defined from plugins
3. Load any action triggers based on machine action called (not action classes)

@param [Hash] env Call environment @return [Builder]

# File lib/vagrant/action/builder.rb, line 291
def apply_action_name(env)
  env[:builder_raw_applied] ||= []
  return self if !env[:action_name]

  hook = Hook.new
  machine_name = env[:machine].name if env[:machine]

  # Start with loading any hook triggers if applicable
  if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") && env[:triggers]
    if !env[:triggers].find(env[:action_name], :before, machine_name, :hook).empty?
      hook.prepend(Vagrant::Action::Builtin::Trigger,
        env[:action_name], env[:triggers], :before, :hook)
    end
    if !env[:triggers].find(env[:action_name], :after, machine_name, :hook).empty?
      hook.append(Vagrant::Action::Builtin::Trigger,
        env[:action_name], env[:triggers], :after, :hook)
    end
  end

  # Next we load up all the action hooks that plugins may
  # have defined
  action_hooks = Vagrant.plugin("2").manager.action_hooks(env[:action_name])
  action_hooks.each do |hook_proc|
    hook_proc.call(hook)
  end

  # Finally load any action triggers defined. The action triggers
  # are the originally implemented trigger style. They run before
  # and after specific provider actions (like :up, :halt, etc) and
  # are different from true action triggers
  if env[:triggers] && !env[:builder_raw_applied].include?(env[:raw_action_name])
    env[:builder_raw_applied] << env[:raw_action_name]

    if !env[:triggers].find(env[:raw_action_name], :before, machine_name, :action, all: true).empty?
      hook.prepend(Vagrant::Action::Builtin::Trigger,
        env[:raw_action_name], env[:triggers], :before, :action, all: true)
    end
    if !env[:triggers].find(env[:raw_action_name], :after, machine_name, :action, all: true).empty?
      # NOTE: These after triggers need to be delayed before running to
      #       allow the rest of the call stack to complete before being
      #       run. The delayed action is prepended to the stack (not appended)
      #       to ensure it is called first, which results in it properly
      #       waiting for everything to finish before itself completing.
      builder = self.class.build(Vagrant::Action::Builtin::Trigger,
        env[:raw_action_name], env[:triggers], :after, :action, all: true)
      hook.prepend(Vagrant::Action::Builtin::Delayed, builder)
    end
  end

  # If the hooks are empty, then there was nothing to apply and
  # we can just send ourself back
  return self if hook.empty?

  # Apply all the hooks to the new builder instance
  hook.apply(self, {
    # Only primary builders run prepend/append, otherwise nested builders
    # would duplicate hooks. See explanation at self#primary.
    no_prepend_or_append: !primary,
  })

  self
end
apply_dynamic_updates(env) click to toggle source

Find any action hooks or triggers which have been defined for items within the stack. Update the stack with any hooks or triggers found.

@param [Hash] env Call environment @return [Builder] self

# File lib/vagrant/action/builder.rb, line 229
def apply_dynamic_updates(env)
  if Vagrant::Util::Experimental.feature_enabled?("typed_triggers")
    triggers = env[:triggers]
  end

  # Use a Hook as a convenient interface for injecting
  # any applicable trigger actions within the stack
  machine_name = env[:machine].name if env[:machine]

  # Iterate over all items in the stack and apply new items
  # into the hook as they are found. Must be sure to dup the
  # stack here since we are modifying the stack in the loop.
  stack.dup.each do |item|
    hook = Hook.new

    action = item.first
    next if action.is_a?(Proc)

    # Start with adding any action triggers that may be defined
    if triggers && !triggers.find(action, :before, machine_name, :action).empty?
      hook.prepend(Vagrant::Action::Builtin::Trigger,
        action.name, triggers, :before, :action)
    end

    if triggers && !triggers.find(action, :after, machine_name, :action).empty?
      hook.append(Vagrant::Action::Builtin::Trigger,
        action.name, triggers, :after, :action)
    end

    # Next look for any hook triggers that may be defined against
    # the dynamically generated action class hooks
    if triggers && !triggers.find(action, :before, machine_name, :hook).empty?
      hook.prepend(Vagrant::Action::Builtin::Trigger,
        action.name, triggers, :before, :hook)
    end

    if triggers && !triggers.find(action, :after, machine_name, :hook).empty?
      hook.append(Vagrant::Action::Builtin::Trigger,
        action.name, triggers, :after, :hook)
    end

    # Finally load any registered hooks for dynamically generated
    # action class based hooks
    Vagrant.plugin("2").manager.find_action_hooks(action).each do |hook_proc|
      hook_proc.call(hook)
    end

    hook.apply(self, root: item)
  end

  # Apply the hook to ourself to update the stack
  self
end
call(env) click to toggle source

Runs the builder stack with the given environment.

# File lib/vagrant/action/builder.rb, line 179
def call(env)
  to_app(env).call(env)
end
delete(index) click to toggle source

Deletes the given middleware object or index

# File lib/vagrant/action/builder.rb, line 173
def delete(index)
  index = self.index(index) unless index.is_a?(Integer)
  stack.delete_at(index)
end
flatten() click to toggle source

Returns a mergeable version of the builder. If ‘use` is called with the return value of this method, then the stack will merge, instead of being treated as a separate single middleware.

# File lib/vagrant/action/builder.rb, line 86
def flatten
  lambda do |env|
    self.call(env)
  end
end
index(object) click to toggle source

Returns the numeric index for the given middleware object.

@param [Object] object The item to find the index for @return [Integer]

# File lib/vagrant/action/builder.rb, line 187
def index(object)
  stack.each_with_index do |item, i|
    return i if item == object
    return i if item.middleware == object
    return i if item.middleware.respond_to?(:name) &&
      item.middleware.name == object
  end

  nil
end
initialize_copy(original) click to toggle source

Implement a custom copy that copies the stack variable over so that we don’t clobber that.

Calls superclass method
# File lib/vagrant/action/builder.rb, line 77
def initialize_copy(original)
  super

  @stack = original.stack.dup
end
insert(idx_or_item, middleware, *args, **keywords, &block) click to toggle source

Inserts a middleware at the given index or directly before the given middleware object.

# File lib/vagrant/action/builder.rb, line 119
def insert(idx_or_item, middleware, *args, **keywords, &block)
  item = StackItem.new(
    middleware: middleware,
    arguments: MiddlewareArguments.new(
      parameters: args,
      keywords: keywords,
      block: block
    )
  )

  if idx_or_item.is_a?(Integer)
    index = idx_or_item
  else
    index = self.index(idx_or_item)
  end

  raise "no such middleware to insert before: #{index.inspect}" unless index

  if middleware.kind_of?(Builder)
    middleware.stack.reverse.each do |stack_item|
      stack.insert(index, stack_item)
    end
  else
    stack.insert(index, item)
  end
end
Also aliased as: insert_before
insert_after(idx_or_item, middleware, *args, **keywords, &block) click to toggle source

Inserts a middleware after the given index or middleware object.

# File lib/vagrant/action/builder.rb, line 149
def insert_after(idx_or_item, middleware, *args, **keywords, &block)
  if idx_or_item.is_a?(Integer)
    index = idx_or_item
  else
    index = self.index(idx_or_item)
  end

  raise "no such middleware to insert after: #{index.inspect}" unless index
  insert(index + 1, middleware, *args, &block)
end
insert_before(idx_or_item, middleware, *args, **keywords, &block)
Alias for: insert
replace(index, middleware, *args, **keywords, &block) click to toggle source

Replaces the given middlware object or index with the new middleware.

# File lib/vagrant/action/builder.rb, line 162
def replace(index, middleware, *args, **keywords, &block)
  if index.is_a?(Integer)
    delete(index)
    insert(index, middleware, *args, **keywords, &block)
  else
    insert_before(index, middleware, *args, **keywords, &block)
    delete(index)
  end
end
to_app(env) click to toggle source

Converts the builder stack to a runnable action sequence.

@param [Hash] env The action environment hash @return [Warden] A callable object

# File lib/vagrant/action/builder.rb, line 202
def to_app(env)
  # Start with a duplicate of ourself which can
  # be modified
  builder = self.dup

  # Apply all dynamic modifications of the stack. This
  # will generate dynamic hooks for all actions within
  # the stack, load any triggers for action classes, and
  # apply them to the builder's stack
  builder.apply_dynamic_updates(env)

  # Now that the stack is fully expanded, apply any
  # action hooks that may be defined so they are on
  # the outermost locations of the stack
  builder.apply_action_name(env)

  # Wrap the middleware stack with the Warden to provide a consistent
  # and predictable behavior upon exceptions.
  Warden.new(builder.stack.dup, env)
end
use(middleware, *args, **keywords, &block) click to toggle source

Adds a middleware class to the middleware stack. Any additional args and a block, if given, are saved and passed to the initializer of the middleware.

@param [Class] middleware The middleware class

# File lib/vagrant/action/builder.rb, line 97
def use(middleware, *args, **keywords, &block)
  item = StackItem.new(
    middleware: middleware,
    arguments: MiddlewareArguments.new(
      parameters: args,
      keywords: keywords,
      block: block
    )
  )

  if middleware.kind_of?(Builder)
    # Merge in the other builder's stack into our own
    self.stack.concat(middleware.stack)
  else
    self.stack << item
  end

  self
end