Separate Vagrant::Action into Runner and Registry
This commit is contained in:
parent
5bfcbcba66
commit
daf711fd80
|
@ -10,134 +10,10 @@ require 'vagrant/action/general'
|
|||
require 'vagrant/action/vm'
|
||||
|
||||
module Vagrant
|
||||
# Manages action running and registration. Every Vagrant environment
|
||||
# has an instance of {Action} to allow for running in the context of
|
||||
# the environment, which is accessible at {Environment#actions}. Actions
|
||||
# are the foundation of most functionality in Vagrant, and are implemented
|
||||
# architecturally as "middleware."
|
||||
#
|
||||
# # Registering an Action
|
||||
#
|
||||
# The main benefits of registering an action is the ability to retrieve and
|
||||
# modify that registered action, as well as easily run the action. An example
|
||||
# of registering an action is shown below, with a simple middleware which just
|
||||
# outputs to `STDOUT`:
|
||||
#
|
||||
# class StdoutMiddleware
|
||||
# def initialize(app, env)
|
||||
# @app = app
|
||||
# end
|
||||
#
|
||||
# def call(env)
|
||||
# puts "HI!"
|
||||
# @app.call(env)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Vagrant::Action.register(:stdout, StdoutMiddleware)
|
||||
#
|
||||
# Then to run a registered action, assuming `env` is a loaded {Environment}:
|
||||
#
|
||||
# env.actions.run(:stdout)
|
||||
#
|
||||
# Or to retrieve the action class for any reason:
|
||||
#
|
||||
# Vagrant::Action[:stdout]
|
||||
#
|
||||
# # Running an Action
|
||||
#
|
||||
# There are various built-in registered actions such as `start`, `stop`, `up`,
|
||||
# etc. Actions are built to be run in the context of an environment, so use
|
||||
# {Environment#actions} to run all actions. Then simply call {#run}:
|
||||
#
|
||||
# env.actions.run(:name)
|
||||
#
|
||||
# Where `:name` is the name of the registered action.
|
||||
#
|
||||
class Action
|
||||
autoload :Environment, 'vagrant/action/environment'
|
||||
autoload :MultiStep, 'vagrant/action/multistep'
|
||||
autoload :Step, 'vagrant/action/step'
|
||||
autoload :Registry, 'vagrant/action/registry'
|
||||
autoload :Runner, 'vagrant/action/runner'
|
||||
autoload :Warden, 'vagrant/action/warden'
|
||||
|
||||
include Util
|
||||
@@reported_interrupt = false
|
||||
|
||||
class << self
|
||||
# Returns the list of registered actions.
|
||||
#
|
||||
# @return [Array]
|
||||
def actions
|
||||
@actions ||= {}
|
||||
end
|
||||
|
||||
# Registers an action and associates it with a symbol. This
|
||||
# symbol can then be referenced in other action builds and
|
||||
# callbacks can be registered on that symbol.
|
||||
#
|
||||
# @param [Symbol] key
|
||||
def register(key, callable)
|
||||
actions[key.to_sym] = callable
|
||||
end
|
||||
|
||||
# Retrieves a registered action by key.
|
||||
#
|
||||
# @param [Symbol] key
|
||||
def [](key)
|
||||
actions[key.to_sym]
|
||||
end
|
||||
end
|
||||
|
||||
# The environment to run the actions in.
|
||||
attr_reader :env
|
||||
|
||||
# Initializes the action with the given environment which the actions
|
||||
# will be run in.
|
||||
#
|
||||
# @param [Environment] env
|
||||
def initialize(env)
|
||||
@env = env
|
||||
@logger = Log4r::Logger.new("vagrant::action")
|
||||
end
|
||||
|
||||
# Runs the given callable object in the context of the environment.
|
||||
# If a symbol is given as the `callable` parameter, then it is looked
|
||||
# up in the registered actions list which are registered with {register}.
|
||||
#
|
||||
# Any options given are injected into the environment hash.
|
||||
#
|
||||
# @param [Object] callable An object which responds to `call`.
|
||||
def run(callable_id, options=nil)
|
||||
callable = callable_id
|
||||
callable = Builder.new.use(callable_id) if callable_id.kind_of?(Class)
|
||||
callable = self.class.actions[callable_id] if callable_id.kind_of?(Symbol)
|
||||
raise ArgumentError, "Argument to run must be a callable object or registered action." if !callable || !callable.respond_to?(:call)
|
||||
|
||||
action_environment = Action::Environment.new(env)
|
||||
action_environment.merge!(options || {})
|
||||
|
||||
# Run the before action run callback, if we're not doing that already
|
||||
run(:before_action_run, action_environment) if callable_id != :before_action_run
|
||||
|
||||
# Run the action chain in a busy block, marking the environment as
|
||||
# interrupted if a SIGINT occurs, and exiting cleanly once the
|
||||
# chain has been run.
|
||||
int_callback = lambda do
|
||||
if action_environment.interrupted?
|
||||
env.ui.error I18n.t("vagrant.actions.runner.exit_immediately")
|
||||
abort
|
||||
end
|
||||
|
||||
env.ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup") if !@@reported_interrupt
|
||||
action_environment.interrupt!
|
||||
@@reported_interrupt = true
|
||||
end
|
||||
|
||||
# We place a process lock around every action that is called
|
||||
@logger.info("Running action: #{callable_id}")
|
||||
env.lock do
|
||||
Busy.busy(int_callback) { callable.call(action_environment) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,7 +36,7 @@ module Vagrant
|
|||
|
||||
# Use the class if it matches the given URI or if this
|
||||
# is the last class...
|
||||
if classes.length == (i + 1) || klass.match?(@env["box"].uri)
|
||||
if classes.length == (i + 1) || klass.match?(@env["box_url"])
|
||||
@env.ui.info I18n.t("vagrant.actions.box.download.with", :class => klass.to_s)
|
||||
@downloader = klass.new(@env)
|
||||
break
|
||||
|
@ -47,7 +47,7 @@ module Vagrant
|
|||
# just in case for now.
|
||||
raise Errors::BoxDownloadUnknownType if !@downloader
|
||||
|
||||
@downloader.prepare(@env["box"].uri)
|
||||
@downloader.prepare(@env["box_url"])
|
||||
true
|
||||
end
|
||||
|
||||
|
@ -76,7 +76,7 @@ module Vagrant
|
|||
end
|
||||
|
||||
def download_to(f)
|
||||
@downloader.download!(@env["box"].uri, f)
|
||||
@downloader.download!(@env["box_url"], f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ module Vagrant
|
|||
# all the necessary Vagrant libraries are loaded. Hopefully
|
||||
# in the future this will no longer be necessary with autoloading.
|
||||
def self.builtin!
|
||||
return
|
||||
# provision - Provisions a running VM
|
||||
register(:provision, Builder.new do
|
||||
use VM::CheckAccessible
|
||||
|
@ -109,7 +110,7 @@ module Vagrant
|
|||
# Other callbacks. There will be more of these in the future. For
|
||||
# now, these are limited to what are needed internally.
|
||||
register(:before_action_run, Builder.new do
|
||||
use General::Validate
|
||||
# use General::Validate
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,32 +4,11 @@ module Vagrant
|
|||
# to the `call` method of each action. This environment contains
|
||||
# some helper methods for accessing the environment as well
|
||||
# as being a hash, to store any additional options.
|
||||
class Environment < Util::HashWithIndifferentAccess
|
||||
# The {Vagrant::Environment} object represented by this
|
||||
# action environment.
|
||||
attr_reader :env
|
||||
|
||||
def initialize(env)
|
||||
super() do |h,k|
|
||||
# By default, try to find the key as a method on the
|
||||
# environment. Gross eval use here.
|
||||
begin
|
||||
value = eval("h.env.#{k}")
|
||||
h[k] = value
|
||||
rescue Exception
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@env = env
|
||||
class Environment < Hash
|
||||
def initialize
|
||||
@interrupted = false
|
||||
end
|
||||
|
||||
# Returns a UI object from the environment
|
||||
def ui
|
||||
env.ui
|
||||
end
|
||||
|
||||
# Marks an environment as interrupted (by an outside signal or
|
||||
# anything). This will trigger any middleware sequences using this
|
||||
# environment to halt. This is automatically set by {Action} when
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
module Vagrant
|
||||
class Action
|
||||
# This is the action registry, which stores action steps indexed
|
||||
# by a unique name. These registry names can be used to call actions
|
||||
# via the `Runner` class.
|
||||
class Registry
|
||||
def initialize
|
||||
@actions = {}
|
||||
end
|
||||
|
||||
# Register a callable by key.
|
||||
#
|
||||
# The callable should be given in a block which will be lazily evaluated
|
||||
# when the action is needed.
|
||||
#
|
||||
# If an action by the given name already exists then it will be
|
||||
# overwritten.
|
||||
def register(key, &block)
|
||||
@actions[key] = block
|
||||
end
|
||||
|
||||
# Get an action by the given key.
|
||||
#
|
||||
# This will evaluate the block given to `register` and return the resulting
|
||||
# action stack.
|
||||
def get(key)
|
||||
return nil if !@actions.has_key?(key)
|
||||
@actions[key].call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
require 'log4r'
|
||||
|
||||
require 'vagrant/util/busy'
|
||||
|
||||
# TODO:
|
||||
# * env.ui
|
||||
# * env.lock
|
||||
|
||||
module Vagrant
|
||||
class Action
|
||||
class Runner
|
||||
@@reported_interrupt = false
|
||||
|
||||
def initialize(registry)
|
||||
@registry = registry
|
||||
@logger = Log4r::Logger.new("vagrant::action::runner")
|
||||
end
|
||||
|
||||
def run(callable_id, options=nil)
|
||||
callable = callable_id
|
||||
callable = Builder.new.use(callable_id) if callable_id.kind_of?(Class)
|
||||
callable = @registry.get(callable_id) if callable_id.kind_of?(Symbol)
|
||||
raise ArgumentError, "Argument to run must be a callable object or registered action." if !callable || !callable.respond_to?(:call)
|
||||
|
||||
# Create the initial environment with the options given
|
||||
environment = Environment.new
|
||||
environment.merge!(options || {})
|
||||
|
||||
# Run the action chain in a busy block, marking the environment as
|
||||
# interrupted if a SIGINT occurs, and exiting cleanly once the
|
||||
# chain has been run.
|
||||
int_callback = lambda do
|
||||
if environment.interrupted?
|
||||
env.ui.error I18n.t("vagrant.actions.runner.exit_immediately")
|
||||
abort
|
||||
end
|
||||
|
||||
env.ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup") if !@@reported_interrupt
|
||||
environment.interrupt!
|
||||
@@reported_interrupt = true
|
||||
end
|
||||
|
||||
# We place a process lock around every action that is called
|
||||
@logger.info("Running action: #{callable_id}")
|
||||
Util::Busy.busy(int_callback) { callable.call(environment) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -37,14 +37,6 @@ module Vagrant
|
|||
directory.join(env.config.vm.box_ovf)
|
||||
end
|
||||
|
||||
# Begins the process of adding a box to the vagrant installation. This
|
||||
# method requires that `name` and `uri` be set. The logic of this method
|
||||
# is kicked out to the `box_add` registered middleware.
|
||||
def add
|
||||
raise Errors::BoxAlreadyExists, :name => name if File.directory?(directory)
|
||||
env.actions.run(:box_add, { "box" => self, "validate" => false })
|
||||
end
|
||||
|
||||
# Begins the process of destroying this box. This cannot be undone!
|
||||
def destroy
|
||||
env.actions.run(:box_remove, { "box" => self, "validate" => false })
|
||||
|
|
|
@ -12,9 +12,10 @@ module Vagrant
|
|||
attr_reader :directory
|
||||
|
||||
# Initializes the class to search for boxes in the given directory.
|
||||
def initialize(directory)
|
||||
def initialize(directory, action_runner)
|
||||
@directory = directory
|
||||
@boxes = []
|
||||
@action_runner = action_runner
|
||||
|
||||
reload!
|
||||
end
|
||||
|
@ -29,6 +30,12 @@ module Vagrant
|
|||
nil
|
||||
end
|
||||
|
||||
# Adds a box to this collection with the given name and located
|
||||
# at the given URL.
|
||||
def add(name, url)
|
||||
@action_runner.run(:box_add, :box_name => name, :box_url => url)
|
||||
end
|
||||
|
||||
# Loads the list of all boxes from the source. This modifies the
|
||||
# current array.
|
||||
def reload!
|
||||
|
|
|
@ -5,7 +5,7 @@ module Vagrant
|
|||
|
||||
desc "add NAME URI", "Add a box to the system"
|
||||
def add(name, uri)
|
||||
Box.add(env, name, uri)
|
||||
env.boxes.add(name, uri)
|
||||
end
|
||||
|
||||
desc "remove NAME", "Remove a box from the system"
|
||||
|
|
|
@ -146,7 +146,7 @@ module Vagrant
|
|||
#
|
||||
# @return [BoxCollection]
|
||||
def boxes
|
||||
@_boxes ||= BoxCollection.new(boxes_path)
|
||||
@_boxes ||= BoxCollection.new(boxes_path, action_runner)
|
||||
end
|
||||
|
||||
# Returns the VMs associated with this environment.
|
||||
|
@ -205,12 +205,18 @@ module Vagrant
|
|||
@host ||= Hosts::Base.load(self, config.vagrant.host)
|
||||
end
|
||||
|
||||
# Returns the {Action} class for this environment which allows actions
|
||||
# to be executed (middleware chains) in the context of this environment.
|
||||
# Action runner for executing actions in the context of this environment.
|
||||
#
|
||||
# @return [Action]
|
||||
def actions
|
||||
@actions ||= Action.new(self)
|
||||
# @return [Action::Runner]
|
||||
def action_runner
|
||||
@action_runner ||= Action::Runner.new(action_registry)
|
||||
end
|
||||
|
||||
# Action registry for registering new actions with this environment.
|
||||
#
|
||||
# @return [Action::Registry]
|
||||
def action_registry
|
||||
@action_registry ||= Action::Registry.new
|
||||
end
|
||||
|
||||
# Loads on initial access and reads data from the global data store.
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
require File.expand_path("../../../base", __FILE__)
|
||||
|
||||
describe Vagrant::Action::Environment do
|
||||
let(:instance) { described_class.new }
|
||||
|
||||
it "should be a hash" do
|
||||
instance.should be_empty
|
||||
instance["foo"] = "bar"
|
||||
instance["foo"].should == "bar"
|
||||
end
|
||||
|
||||
it "should keep track of interrupted state" do
|
||||
instance.should_not be_interrupted
|
||||
instance.interrupt!
|
||||
instance.should be_interrupted
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
require File.expand_path("../../../base", __FILE__)
|
||||
|
||||
describe Vagrant::Action::Registry do
|
||||
let(:instance) { described_class.new }
|
||||
|
||||
it "should return nil for nonexistent actions" do
|
||||
instance.get("foo").should be_nil
|
||||
end
|
||||
|
||||
it "should register an action without calling the block yet" do
|
||||
expect do
|
||||
instance.register("foo") do
|
||||
raise Exception, "BOOM!"
|
||||
end
|
||||
end.to_not raise_error
|
||||
end
|
||||
|
||||
it "should call and return the result of a block when asking for the action" do
|
||||
object = Object.new
|
||||
instance.register("foo") do
|
||||
object
|
||||
end
|
||||
|
||||
instance.get("foo").should eql(object)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
require File.expand_path("../../../base", __FILE__)
|
||||
|
||||
describe Vagrant::Action::Runner do
|
||||
let(:registry) do
|
||||
d = double("registry")
|
||||
d.stub(:get)
|
||||
d
|
||||
end
|
||||
|
||||
let(:instance) { described_class.new(registry) }
|
||||
|
||||
it "should raise an error if an invalid callable is given" do
|
||||
expect { instance.run(7) }.to raise_error(ArgumentError, /must be a callable/)
|
||||
end
|
||||
|
||||
it "should be able to use a Proc as a callable" do
|
||||
callable = Proc.new { raise Exception, "BOOM" }
|
||||
expect { instance.run(callable) }.to raise_error(Exception, "BOOM")
|
||||
end
|
||||
|
||||
it "should be able to use a Class as a callable" do
|
||||
callable = Class.new do
|
||||
def initialize(app, env)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
raise Exception, "BOOM"
|
||||
end
|
||||
end
|
||||
|
||||
expect { instance.run(callable) }.to raise_error(Exception, "BOOM")
|
||||
end
|
||||
|
||||
it "should pass options into hash given to callable" do
|
||||
result = nil
|
||||
callable = lambda do |env|
|
||||
result = env["data"]
|
||||
end
|
||||
|
||||
instance.run(callable, "data" => "foo")
|
||||
result.should == "foo"
|
||||
end
|
||||
end
|
|
@ -4,7 +4,8 @@ describe Vagrant::BoxCollection do
|
|||
include_context "unit"
|
||||
|
||||
let(:environment) { isolated_environment }
|
||||
let(:instance) { described_class.new(environment.boxes_dir) }
|
||||
let(:action_runner) { double("action runner") }
|
||||
let(:instance) { described_class.new(environment.boxes_dir, action_runner) }
|
||||
|
||||
it "should list all available boxes" do
|
||||
# No boxes yet.
|
||||
|
@ -29,4 +30,18 @@ describe Vagrant::BoxCollection do
|
|||
result.name.should == "foo"
|
||||
end
|
||||
end
|
||||
|
||||
it "should add the box" do
|
||||
name = "foo"
|
||||
url = "bar"
|
||||
|
||||
# Test the invocation of the action runner with the proper name
|
||||
# and parameters. We leave the testing of the actual stack to
|
||||
# acceptance tests, and individual pieces to unit tests of each
|
||||
# step.
|
||||
options = { :box_name => name, :box_url => url }
|
||||
action_runner.should_receive(:run).with(:box_add, options)
|
||||
|
||||
instance.add(name, url)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class ActionEnvironmentTest < Test::Unit::TestCase
|
||||
setup do
|
||||
@klass = Vagrant::Action::Environment
|
||||
@instance = @klass.new(vagrant_env)
|
||||
end
|
||||
|
||||
should "be a hash with indifferent access" do
|
||||
assert @instance.is_a?(Vagrant::Util::HashWithIndifferentAccess)
|
||||
end
|
||||
|
||||
should "default values to those on the env" do
|
||||
@instance.env.stubs(:key).returns("value")
|
||||
assert_equal "value", @instance["key"]
|
||||
end
|
||||
|
||||
should "setup the UI" do
|
||||
assert_equal @instance.env.ui, @instance.ui
|
||||
end
|
||||
|
||||
should "report interrupted if interrupt error" do
|
||||
assert !@instance.interrupted?
|
||||
@instance.interrupt!
|
||||
assert @instance.interrupted?
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue