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

View file

@ -1,4 +1,5 @@
require 'set' require 'set'
require_relative 'servers/role_filter'
module Capistrano module Capistrano
class Configuration class Configuration
class Servers class Servers
@ -40,17 +41,21 @@ module Capistrano
servers.find_all { |server| server.has_role? role} servers.find_all { |server| server.has_role? role}
end end
def fetch_roles(names, options) def fetch_roles(required, options)
if Array(names).flatten.map(&:to_sym).include?(:all) filter_roles = RoleFilter.for(required, available_roles)
filter(servers, options) select(servers_with_roles(filter_roles), options)
else
role_servers = Array(names).flat_map { |name| fetch name }.uniq
filter(role_servers, options)
end
end end
def filter(servers, options) def servers_with_roles(roles)
Filter.new(servers, options).filtered_servers 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 end
def servers def servers
@ -60,48 +65,6 @@ module Capistrano
def extract_options(array) def extract_options(array)
array.last.is_a?(::Hash) ? array.pop : {} array.last.is_a?(::Hash) ? array.pop : {}
end 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 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}", mirror_exists: "The repository mirror is at %{at}",
revision_log_message: 'Branch %{branch} deployed as release %{release} by %{user}', revision_log_message: 'Branch %{branch} deployed as release %{release} by %{user}',
rollback_log_message: '%{user} rolled back to release %{release}', rollback_log_message: '%{user} rolled back to release %{release}',
filter_removes_all_servers: 'Your filter `%{filter}` would remove all matching servers',
console: { console: {
welcome: 'capistrano console - enter command to execute on %{stage}', welcome: 'capistrano console - enter command to execute on %{stage}',
bye: 'bye' bye: 'bye'

View file

@ -134,6 +134,75 @@ module Capistrano
end end
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 describe 'assign ssh_options' do
let(:server) { Server.new('user_name@hostname') } 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 end
describe '#roles' do describe 'selecting roles' do
before do before do
servers.add_host('1', roles: :app, active: true) servers.add_host('1', roles: :app, active: true)
servers.add_host('2', roles: :app) servers.add_host('2', roles: :app)
end end
it 'raises if the filter would remove all matching hosts' do it 'is empty if the filter would remove all matching hosts' do
I18n.expects(:t) expect(servers.roles_for([:app, select: :inactive])).to be_empty
expect { servers.roles_for([:app, select: :inactive]) }.to raise_error
end end
it 'can filter hosts by properties on the host object using symbol as shorthand' do it 'can filter hosts by properties on the host object using symbol as shorthand' do
@ -129,19 +128,57 @@ module Capistrano
end end
it 'can filter hosts by properties on the host using a regular proc' do 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 end
it 'can select hosts by properties on the host using a regular proc' do 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 end
it 'raises if the regular proc filter would remove all matching hosts' do it 'is empty if the regular proc filter would remove all matching hosts' do
I18n.expects(:t) expect(servers.roles_for([:app, select: ->(h) { h.properties.inactive }])).to be_empty
expect { servers.roles_for([:app, select: lambda { |h| h.properties.inactive }])}.to raise_error 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
end end
end end
end