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:
parent
b4f94ee43f
commit
bef71c4e68
7 changed files with 401 additions and 69 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
86
lib/capistrano/configuration/servers/role_filter.rb
Normal file
86
lib/capistrano/configuration/servers/role_filter.rb
Normal 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
|
|
@ -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'
|
||||
|
|
|
@ -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') }
|
||||
|
||||
|
|
140
spec/lib/capistrano/configuration/servers/role_filter_spec.rb
Normal file
140
spec/lib/capistrano/configuration/servers/role_filter_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue