1
0
Fork 0
mirror of https://github.com/capistrano/capistrano synced 2023-03-27 23:21:18 -04:00

Dynamic roles (closes #11107, thanks for the awesome patch!)

git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@8905 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Jamis Buck 2008-02-19 23:39:26 +00:00
parent 76957eeeef
commit 3cbc3496c8
4 changed files with 209 additions and 5 deletions

View file

@ -1,5 +1,7 @@
*SVN*
* Dynamic roles (e.g. role(:app) { "host.name" }) [dmasover]
* Implement Bzr#next_revision so that pending changes can be reported correctly [casret]
* Use a proper export command for bzr SCM [drudru]

View file

@ -1,4 +1,5 @@
require 'capistrano/server_definition'
require 'capistrano/role'
module Capistrano
class Configuration
@ -15,7 +16,7 @@ module Capistrano
def initialize_with_roles(*args) #:nodoc:
initialize_without_roles(*args)
@roles = Hash.new { |h,k| h[k] = [] }
@roles = Hash.new { |h,k| h[k] = Role.new }
end
# Define a new role and its associated servers. You must specify at least
@ -41,9 +42,10 @@ module Capistrano
# that call to "role":
#
# role :web, "web2", "web3", :user => "www", :port => 2345
def role(which, *args)
def role(which, *args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
which = which.to_sym
roles[which].push(block, options) if block_given?
args.each { |host| roles[which] << ServerDefinition.new(host, options) }
end
end

112
lib/capistrano/role.rb Normal file
View file

@ -0,0 +1,112 @@
module Capistrano
class Role
include Enumerable
def initialize(*list)
@static_servers = []
@dynamic_servers = []
push(*list)
end
def each(&block)
servers.each &block
end
def push(*list)
options = list.last.is_a?(Hash) ? list.pop : {}
list.each do |item|
if item.respond_to?(:call)
@dynamic_servers << DynamicServerList.new(item, options)
else
@static_servers << self.class.wrap_server(item, options)
end
end
end
alias_method :<<, :push
def servers
@static_servers + dynamic_servers
end
alias_method :to_ary, :servers
def empty?
servers.empty?
end
# Resets the cache, so that proc values may be recalculated.
# There should be a command in Configuration::Roles to do this,
# but I haven't needed it yet, and I'm not sure yet
# what to call that command. Suggestions?
def reset!
@dynamic_servers.each { |item| item.reset! }
end
# Clears everything. I still thing this should be 'clear!', but that's not
# the way Array does it.
def clear
@dynamic_servers.clear
@static_servers.clear
end
# Mostly for documentation purposes. Doesn't seem to do anything.
protected
# This is the combination of a block, a hash of options, and a cached value.
# It is protected because it is an implementation detail -- the original
# implementation was two lists (blocks and cached results of calling them).
class DynamicServerList
def initialize (block, options)
@block = block
@options = options
@cached = []
@is_cached = false
end
# Convert to a list of ServerDefinitions
def to_ary
unless @is_cached
@cached = Role::wrap_list(@block.call(@options), @options)
@is_cached = true
end
@cached
end
# Clear the cached value
def reset!
@cached.clear
@is_cached = false
end
end
# Attribute reader for the cached results of executing the blocks in turn
def dynamic_servers
@dynamic_servers.inject([]) { |list, item| list.concat item }
end
# Wraps a string in a ServerDefinition, if it isn't already.
# This and wrap_list should probably go in ServerDefinition in some form.
def self.wrap_server (item, options)
item.is_a?(ServerDefinition) ? item : ServerDefinition.new(item, options)
end
# Turns a list, or something resembling a list, into a properly-formatted
# ServerDefinition list. Keep an eye on this one -- it's entirely too
# magical for its own good. In particular, if ServerDefinition ever inherits
# from Array, this will break.
def self.wrap_list (*list)
options = list.last.is_a?(Hash) ? list.pop : {}
if list.length == 1
if list.first.nil?
return []
elsif list.first.is_a?(Array)
list = list.first
end
end
options.merge! list.pop if list.last.is_a?(Hash)
list.map do |item|
self.wrap_server item, options
end
end
end
end

View file

@ -1,5 +1,6 @@
require "#{File.dirname(__FILE__)}/../utils"
require 'capistrano/configuration/roles'
require 'capistrano/server_definition'
class ConfigurationRolesTest < Test::Unit::TestCase
class MockConfig
@ -16,6 +17,10 @@ class ConfigurationRolesTest < Test::Unit::TestCase
@config = MockConfig.new
end
def assert_role_equals(list)
assert_equal list, @config.roles[:app].map { |s| s.host }
end
def test_initialize_should_initialize_roles_collection
assert @config.original_initialize_called
assert @config.roles.empty?
@ -29,13 +34,34 @@ class ConfigurationRolesTest < Test::Unit::TestCase
def test_role_with_one_argument_should_add_to_roles_collection
@config.role :app, "app1.capistrano.test"
assert_equal [:app], @config.roles.keys
assert_equal %w(app1.capistrano.test), @config.roles[:app].map { |s| s.host }
assert_role_equals %w(app1.capistrano.test)
end
def test_role_block_returning_single_string_is_added_to_roles_collection
@config.role :app do
'app1.capistrano.test'
end
assert_role_equals %w(app1.capistrano.test)
end
def test_role_with_multiple_arguments_should_add_each_to_roles_collection
@config.role :app, "app1.capistrano.test", "app2.capistrano.test"
assert_equal [:app], @config.roles.keys
assert_equal %w(app1.capistrano.test app2.capistrano.test), @config.roles[:app].map { |s| s.host }
assert_role_equals %w(app1.capistrano.test app2.capistrano.test)
end
def test_role_with_block_and_strings_should_add_both_to_roles_collection
@config.role :app, 'app1.capistrano.test' do
'app2.capistrano.test'
end
assert_role_equals %w(app1.capistrano.test app2.capistrano.test)
end
def test_role_block_returning_array_should_add_each_to_roles_collection
@config.role :app do
['app1.capistrano.test', 'app2.capistrano.test']
end
assert_role_equals %w(app1.capistrano.test app2.capistrano.test)
end
def test_role_with_options_should_apply_options_to_each_argument
@ -44,4 +70,66 @@ class ConfigurationRolesTest < Test::Unit::TestCase
assert_equal({:extra => :value}, server.options)
end
end
end
def test_role_with_options_should_apply_options_to_block_results
@config.role :app, :extra => :value do
['app1.capistrano.test', 'app2.capistrano.test']
end
@config.roles[:app].each do |server|
assert_equal({:extra => :value}, server.options)
end
end
def test_options_should_apply_only_to_this_argument_set
@config.role :app, 'app1.capistrano.test', 'app2.capistrano.test' do
['app3.capistrano.test', 'app4.capistrano.test']
end
@config.role :app, 'app5.capistrano.test', 'app6.capistrano.test', :extra => :value do
['app7.capistrano.test', 'app8.capistrano.test']
end
@config.role :app, 'app9.capistrano.test'
option_hosts = ['app5.capistrano.test', 'app6.capistrano.test', 'app7.capistrano.test', 'app8.capistrano.test']
@config.roles[:app].each do |server|
if (option_hosts.include? server.host)
assert_equal({:extra => :value}, server.options)
else
assert_not_equal({:extra => :value}, server.options)
end
end
end
# Here, the source should be more readable than the method name
def test_role_block_returns_options_hash_is_merged_with_role_options_argument
@config.role :app, :first => :one, :second => :two do
['app1.capistrano.test', 'app2.capistrano.test', {:second => :please, :third => :three}]
end
@config.roles[:app].each do |server|
assert_equal({:first => :one, :second => :please, :third => :three}, server.options)
end
end
def test_role_block_can_override_role_options_argument
@config.role :app, :value => :wrong do
Capistrano::ServerDefinition.new('app.capistrano.test')
end
@config.roles[:app].servers
@config.roles[:app].servers.each do |server|
assert_not_equal({:value => :wrong}, server.options)
end
end
def test_role_block_can_return_nil
@config.role :app do
nil
end
assert_role_equals ([])
end
def test_role_block_can_return_empty_array
@config.role :app do
[]
end
assert_role_equals ([])
end
end