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

Implement ROLE filter

Target roles by including an env variable:

    ROLES=app,web cap production deploy

Or by setting `filter` or `select` variables:

    set :filter, roles: %w{app db}

Both variables will be applied if both are defined

Closes #446, depends on https://github.com/leehambley/sshkit/pull/29
This commit is contained in:
seenmyfate 2013-08-23 09:50:55 +01:00
parent b4f94ee43f
commit bef71c4e68
7 changed files with 401 additions and 69 deletions

View file

@ -22,6 +22,11 @@ module Capistrano
hostname == Server.new(host).hostname
end
def select?(options)
selector = Selector.new(options)
selector.call(self)
end
def primary
self if fetch(:primary)
end
@ -41,6 +46,20 @@ module Capistrano
alias_method :netssh_options_without_options, :netssh_options
alias_method :netssh_options, :netssh_options_with_options
def roles_array
roles.to_a
end
private
def add_property(key, value)
if respond_to?("#{key}=")
send("#{key}=", value)
else
set(key, value)
end
end
class Properties
def initialize
@ -79,15 +98,34 @@ module Capistrano
end
private
def add_property(key, value)
if respond_to?("#{key}=")
send("#{key}=", value)
else
set(key, value)
class Selector
def initialize(options)
@options = options
end
def callable
if key.respond_to?(:call)
key
else
->(server) { server.fetch(key) }
end
end
def call(server)
callable.call(server)
end
private
attr_reader :options
def key
options[:filter] || options[:select] || all
end
def all
->(server) { :all }
end
end
end

View file

@ -1,4 +1,5 @@
require 'set'
require_relative 'servers/role_filter'
module Capistrano
class Configuration
class Servers
@ -40,17 +41,21 @@ module Capistrano
servers.find_all { |server| server.has_role? role}
end
def fetch_roles(names, options)
if Array(names).flatten.map(&:to_sym).include?(:all)
filter(servers, options)
else
role_servers = Array(names).flat_map { |name| fetch name }.uniq
filter(role_servers, options)
end
def fetch_roles(required, options)
filter_roles = RoleFilter.for(required, available_roles)
select(servers_with_roles(filter_roles), options)
end
def filter(servers, options)
Filter.new(servers, options).filtered_servers
def servers_with_roles(roles)
roles.flat_map { |role| fetch role }.uniq
end
def select(servers, options)
servers.select { |server| server.select?(options) }
end
def available_roles
servers.flat_map { |server| server.roles_array }.uniq
end
def servers
@ -60,48 +65,6 @@ module Capistrano
def extract_options(array)
array.last.is_a?(::Hash) ? array.pop : {}
end
class Filter
def initialize(servers, options)
@servers, @options = servers, options
end
def filtered_servers
if servers_with_filter.any?
servers_with_filter
else
fail I18n.t(:filter_removes_all_servers, scope: :capistrano, filter: key || '(no filter)' )
end
end
private
attr_reader :options, :servers
def servers_with_filter
@servers_with_filter ||= servers.select(&filter)
end
def key
options[:filter] || options[:select]
end
def filter_option
key || all
end
def filter
if filter_option.respond_to?(:call)
filter_option
else
lambda { |server| server.fetch(filter_option) }
end
end
def all
lambda { |server| :all }
end
end
end
end
end

View file

@ -0,0 +1,86 @@
module Capistrano
class Configuration
class Servers
class RoleFilter
def initialize(required, available)
@required, @available = required, available
end
def self.for(required, available)
new(required, available).roles
end
def roles
if required.include?(:all)
available
else
required.select { |name| available.include? name }
end
end
private
def required
Array(@required).flat_map(&:to_sym)
end
def available
if role_filter.any?
role_filter
else
@available
end
end
def role_filter
env_filter | configuration_filter
end
def configuration_filter
ConfigurationFilter.new.roles
end
def env_filter
EnvFilter.new.roles
end
class ConfigurationFilter
def roles
if filter
Array(filter.fetch(:roles, [])).map(&:to_sym)
else
[]
end
end
def config
Configuration.env
end
def filter
config.fetch(:filter) || config.fetch(:select)
end
end
class EnvFilter
def roles
if filter
filter.split(',').map(&:to_sym)
else
[]
end
end
def filter
ENV['ROLES']
end
end
end
end
end
end

View file

@ -18,7 +18,6 @@ en = {
mirror_exists: "The repository mirror is at %{at}",
revision_log_message: 'Branch %{branch} deployed as release %{release} by %{user}',
rollback_log_message: '%{user} rolled back to release %{release}',
filter_removes_all_servers: 'Your filter `%{filter}` would remove all matching servers',
console: {
welcome: 'capistrano console - enter command to execute on %{stage}',
bye: 'bye'

View file

@ -134,6 +134,75 @@ module Capistrano
end
end
describe '#include?' do
let(:options) { {} }
subject { server.select?(options) }
before do
server.properties.active = true
end
context 'options are empty' do
it { should be_true }
end
context 'value is a symbol' do
context 'value matches server property' do
context 'with :filter' do
let(:options) { { filter: :active }}
it { should be_true }
end
context 'with :select' do
let(:options) { { select: :active }}
it { should be_true }
end
end
context 'value does not match server properly' do
context 'with :filter' do
let(:options) { { filter: :inactive }}
it { should be_false }
end
context 'with :select' do
let(:options) { { select: :inactive }}
it { should be_false }
end
end
end
context 'value is a proc' do
context 'value matches server property' do
context 'with :filter' do
let(:options) { { filter: ->(s) { s.properties.active } } }
it { should be_true }
end
context 'with :select' do
let(:options) { { select: ->(s) { s.properties.active } } }
it { should be_true }
end
end
context 'value does not match server properly' do
context 'with :filter' do
let(:options) { { filter: ->(s) { s.properties.inactive } } }
it { should be_false }
end
context 'with :select' do
let(:options) { { select: ->(s) { s.properties.inactive } } }
it { should be_false }
end
end
end
end
describe 'assign ssh_options' do
let(:server) { Server.new('user_name@hostname') }

View file

@ -0,0 +1,140 @@
require 'spec_helper'
module Capistrano
class Configuration
class Servers
describe RoleFilter do
let(:role_filter) { RoleFilter.new(required, available) }
let(:required) { [] }
let(:available) { [:web, :app, :db] }
describe '#new' do
it 'takes two arrays of role names' do
expect(role_filter)
end
end
describe '.for' do
subject { RoleFilter.for(required, available) }
context 'without env vars' do
context ':all required' do
let(:required) { [:all] }
it 'returns all available names' do
expect(subject).to eq available
end
end
context 'role names required' do
let(:required) { [:web, :app] }
it 'returns all required names' do
expect(subject).to eq required
end
end
end
context 'with ENV vars' do
before do
ENV.stubs(:[]).with('ROLES').returns('app,web')
end
context ':all required' do
let(:required) { [:all] }
it 'returns available names defined in ROLES' do
expect(subject).to eq [:app, :web]
end
end
context 'role names required' do
let(:required) { [:web, :db] }
it 'returns all required names defined in ROLES' do
expect(subject).to eq [:web]
end
end
end
context 'with configuration filters' do
before do
Configuration.env.set(:filter, roles: %w{app web})
end
context ':all required' do
let(:required) { [:all] }
it 'returns available names defined in the filter' do
expect(subject).to eq [:app, :web]
end
end
context 'role names required' do
let(:required) { [:web, :db] }
it 'returns all required names defined in the filter' do
expect(subject).to eq [:web]
end
end
after do
Configuration.env.delete(:filter)
end
end
context 'with a single configuration filter' do
before do
Configuration.env.set(:filter, roles: 'web')
end
context ':all required' do
let(:required) { [:all] }
it 'returns available names defined in the filter' do
expect(subject).to eq [:web]
end
end
context 'role names required' do
let(:required) { [:web, :db] }
it 'returns all required names defined in the filter' do
expect(subject).to eq [:web]
end
end
after do
Configuration.env.delete(:filter)
end
end
context 'with configuration filters and ENV vars' do
before do
Configuration.env.set(:filter, roles: %w{app})
ENV.stubs(:[]).with('ROLES').returns('web')
end
context ':all required' do
let(:required) { [:all] }
it 'returns available names defined in the filter' do
expect(subject).to eq [:web, :app]
end
end
context 'role names required' do
let(:required) { [:web, :db] }
it 'returns all required names defined in the filter' do
expect(subject).to eq [:web]
end
end
after do
Configuration.env.delete(:filter)
end
end
end
end
end
end
end

View file

@ -108,16 +108,15 @@ module Capistrano
end
describe '#roles' do
describe 'selecting roles' do
before do
servers.add_host('1', roles: :app, active: true)
servers.add_host('2', roles: :app)
end
it 'raises if the filter would remove all matching hosts' do
I18n.expects(:t)
expect { servers.roles_for([:app, select: :inactive]) }.to raise_error
it 'is empty if the filter would remove all matching hosts' do
expect(servers.roles_for([:app, select: :inactive])).to be_empty
end
it 'can filter hosts by properties on the host object using symbol as shorthand' do
@ -129,19 +128,57 @@ module Capistrano
end
it 'can filter hosts by properties on the host using a regular proc' do
expect(servers.roles_for([:app, filter: lambda { |h| h.properties.active }]).length).to eq 1
expect(servers.roles_for([:app, filter: ->(h) { h.properties.active }]).length).to eq 1
end
it 'can select hosts by properties on the host using a regular proc' do
expect(servers.roles_for([:app, select: lambda { |h| h.properties.active }]).length).to eq 1
expect(servers.roles_for([:app, select: ->(h) { h.properties.active }]).length).to eq 1
end
it 'raises if the regular proc filter would remove all matching hosts' do
I18n.expects(:t)
expect { servers.roles_for([:app, select: lambda { |h| h.properties.inactive }])}.to raise_error
it 'is empty if the regular proc filter would remove all matching hosts' do
expect(servers.roles_for([:app, select: ->(h) { h.properties.inactive }])).to be_empty
end
end
describe 'filtering roles' do
before do
ENV.stubs(:[]).with('ROLES').returns('web,db')
servers.add_host('1', roles: :app, active: true)
servers.add_host('2', roles: :app)
servers.add_host('3', roles: :web)
servers.add_host('4', roles: :web)
servers.add_host('5', roles: :db)
end
subject { servers.roles_for(roles).map(&:hostname) }
context 'when selecting all roles' do
let(:roles) { [:all] }
it 'returns the roles specified by ROLE' do
expect(subject).to eq %w{3 4 5}
end
end
context 'when selecting roles included in ROLE' do
let(:roles) { [:app, :web] }
it 'returns only roles that match ROLE' do
expect(subject).to eq %w{3 4}
end
end
context 'when selecting roles not included in ROLE' do
let(:roles) { [:app] }
it 'is empty' do
expect(subject).to be_empty
end
end
end
end
end
end