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
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
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}",
|
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'
|
||||||
|
|
|
@ -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') }
|
||||||
|
|
||||||
|
|
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
|
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
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue