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

Treat all host/role filtering options the same

Command line —host and —role options
Environment variables ROLES and HOSTS
Filter variable hash keys :role and :host
This commit is contained in:
Nick Townsend 2014-09-24 20:12:14 -07:00
parent 5ba2c55b69
commit ede09c93e8
9 changed files with 146 additions and 100 deletions

View file

@ -7,12 +7,27 @@ Reverse Chronological Order:
https://github.com/capistrano/capistrano/compare/v3.2.1...HEAD https://github.com/capistrano/capistrano/compare/v3.2.1...HEAD
* Enhancements (@townsen) * Enhancements (@townsen)
* _External_ Host and Role filtering now affects only `on()` commands * Previously filtering would affect any generated configuration files so that
files newly deployed would not be the same as those on the hosts previously
deployed (and now excluded by filters). This is almost certainly not what is
wanted: the filters should apply only to the on() method and thus any
configuration files deployed will be identical across the set of servers
making up the stage.
* Host and Role filtering now affects only `on()` commands
and not the `roles()`, `release_roles()` and `primary()` methods. and not the `roles()`, `release_roles()` and `primary()` methods.
* _Internal_ Host and Role filtering affects the `roles()`, `release_roles()` * This applies to filters defined via the command line, the environment
and `primary()` methods. and the :filter variable.
* Host and Role filtering now supports Regular expressions * Filtering now supports Regular expressions
* See the README.md file for a comprehensive discussion of these changes * This change _could_ cause existing scripts that use filtering and depend on
the old behaviour to fail, though it is unlikely. Users who rely on
filtering should check that generated configuration files are correct, and
where not introduce server properties to do the filtering. For example, if a
filter was used to specify an active subset of servers (by hostname), it should
be removed and replaced with an 'active' property (set to true or false) on the
server definitions. This keeps the stage file as the canonical model of the
deployment environment.
* See the documentation in the README.md file
* Pushing again to trigger another build (I have a seemingly random build fail) (@townsen) * Pushing again to trigger another build (I have a seemingly random build fail) (@townsen)
* Enhancements (@townsen) * Enhancements (@townsen)

View file

@ -272,58 +272,37 @@ __Support removed__ for following variables:
Capistrano enables the declaration of servers and roles, each of which may have properties Capistrano enables the declaration of servers and roles, each of which may have properties
associated with them. Tasks are then able to use these definitions in two distinct ways: associated with them. Tasks are then able to use these definitions in two distinct ways:
* To execute commands on remote hosts: using the `on()` method (provided by SSHKit), and * To determine _configurations_: typically by using the `roles()`, `release_roles()` and
* To determine configurations: typically by using the `roles()` method (and relatives) outside the scope of `on()` `primary()` methods. Typically these are used outside the scope of the `on()` method.
An example of the latter would be to create a list of available web servers in order to * To _interact_ with remote hosts using the `on()` method
automate the setup of an F5 pool.
An example of the two would be to create a `/etc/krb5.conf' file containing the list of
available KDC's by using the list of servers returned by `roles(:kdc)` and then uploading
it to all client machines using `on(roles(:all)) do upload!(file) end`
A problem with this arises when _filters_ are used. Filters are designed to limit the A problem with this arises when _filters_ are used. Filters are designed to limit the
actual set of hosts that are used to a subset of those in the overall stage, but how actual set of hosts that are used to a subset of those in the overall stage, but how
should that apply to the above? should that apply in the above case?
If the filter applies to both the _command_ and _configuration_ aspects, any configuration If the filter applies to both the _interaction_ and _configuration_ aspects, any configuration
files deployed will not be the same as those on the hosts excluded by the filters. On the files deployed will not be the same as those on the hosts excluded by the filters. This is
other hand if the filter applies only to the _command_ aspect, then any configuration almost certainly not what is wanted, the filters should apply only to the _interactions_
files deployed will be identical across the stage. ensuring that any configuration files deployed will be identical across the stage.
Consider also the different ways in which filters may be specified. Externally: Another type of filtering is done by defining properties on servers and selecting on that
* Via environment variables HOSTS and ROLES basis. An example of that is the 'no_release' property and it's use in the
* Via command line options `--hosts` and `--roles` `release_roles()` method. To distinguish these two types of filtering we name them:
And internally:
* Via the `:filter` variable
* Via options passed to the `roles()` method (and implicitly in methods like `release_roles()`)
Currently, when a filter is applied via __any__ of the current methods, it * On-Filtering
affects __both__ the _command_ and the _configuration_ aspects. Specified in the following ways:
* Via environment variables HOSTS and ROLES
* Via command line options `--hosts` and `--roles`
* Via the `:filter` variable set in a stage file
* Property-Filtering
These are specified by options passed to the `roles()` method (and implicitly in methods
like `release_roles()` and `primary()`)
For practical uses this behaviour needs refining. A core principle of Capistrano is that
the stage file is the complete embodiment of the configuration (possibly including
settings from deploy.rb and Capfile), and therefore any filtering of the configuration
should be declared there. Put the other way, an external filter, done for the purposes of
limiting which hosts commands are executed on, should not affect the overall
configuration.
So this fix makes the external filters only apply to commands issued: ie. they restrict
the hosts that an `on()` method will use, the will not affect the `roles()` method. On the
other hand internal filters will always apply
By making this distinction two distinct usage models can be catered for. When a subset of
servers and/or roles are deployed to:
* With a configuration that reflects only that subset. This can be achieved by
using the `set :filter, hosts: [a,b], roles: [:web,:app]` approach.
* With a configuration that reflects the entire stage. This is useful when deploying a new
server to an existing configuration. This can be achieved by using either of the
external filtering methods.
The former corresponds to the existing behaviour, although done slightly differently.
The latter is new behaviour which is a more common use case.
We also change external filters so that they can use regular expressions. If either
a host or role name in a filter doesn't match `/^[-\w.]*$/` then it's assumed to be
a regular expression.
To increase the utility of On-Filters they can use regular expressions: To increase the utility of On-Filters they can use regular expressions:
* If the host name in a filter doesn't match `/^[-A-Za-z0-9.]+$/` (the set of valid characters * If the host name in a filter doesn't match `/^[-A-Za-z0-9.]+$/` (the set of valid characters
for a DNS name) then it's assumed to be a regular expression. for a DNS name) then it's assumed to be a regular expression.
@ -331,14 +310,9 @@ To increase the utility of On-Filters they can use regular expressions:
of them to be specified on one line we use the comma. To use a regexp for a role filter begin of them to be specified on one line we use the comma. To use a regexp for a role filter begin
and end the string with '/'. These may not contain a comma. and end the string with '/'. These may not contain a comma.
When multiple filters are specified in the same declaration, the final filter is the When filters are specified using comma separated lists, the final filter is the _union_ of
_union_ of all of the components, so an implicit OR is between each one. However when all of the components. However when multiple filters are declared the result is the
multiple filters are declared, they are evaluated in the order declared and so are ANDed _intersection_.
together. The order of processing is:
* Environment variables,
* Command line options,
* The `:filter` variable value in effect at the time of the `on()` call
## SSHKit ## SSHKit

View file

@ -1,12 +1,5 @@
require 'rake' require 'rake'
require 'sshkit' require 'sshkit'
require 'sshkit/dsl'
module SSHKit
module DSL
alias_method :sshkit_on, :on
end
end
require 'io/console' require 'io/console'

View file

@ -40,11 +40,6 @@ module Capistrano
opts.separator "Invoke (or simulate invoking) a task:" opts.separator "Invoke (or simulate invoking) a task:"
opts.separator " bundle exec cap [--dry-run] STAGE TASK" opts.separator " bundle exec cap [--dry-run] STAGE TASK"
opts.separator "" opts.separator ""
opts.separator "Host and Role Filters:"
opts.separator " Host and role patterns may be specified as a comma separated list"
opts.separator " each item of which is treated either as a literal match (if it contains"
opts.separator " just the characters A-Za-z0-9-_.) or a regular expression (otherwise)."
opts.separator ""
opts.separator "Advanced options:" opts.separator "Advanced options:"
opts.on_tail("-h", "--help", "-H", "Display this help message.") do opts.on_tail("-h", "--help", "-H", "Display this help message.") do
@ -112,18 +107,18 @@ module Capistrano
def roles def roles
['--roles ROLES', '-r', ['--roles ROLES', '-r',
"Run SSH commands only on hosts matching these roles (see syntax above)", "Run SSH commands only on hosts matching these roles",
lambda { |value| lambda { |value|
Configuration.env.add_external_filter(:role, value.split(",")) Configuration.env.add_cmdline_filter(:role, value)
} }
] ]
end end
def hostfilter def hostfilter
['--hosts HOSTS', '-z', ['--hosts HOSTS', '-z',
"Run SSH commands only on matching hosts (see syntax above)", "Run SSH commands only on matching hosts",
lambda { |value| lambda { |value|
Configuration.env.add_external_filter(:host, value.split(",")) Configuration.env.add_cmdline_filter(:host, value)
} }
] ]
end end

View file

@ -87,16 +87,30 @@ module Capistrano
@timestamp ||= Time.now.utc @timestamp ||= Time.now.utc
end end
def add_external_filter(type, values) def setup_filters
external_filters << Filter.new(type, values) @filters = cmdline_filters.clone
@filters << Filter.new(:role, ENV['ROLES']) if ENV['ROLES']
@filters << Filter.new(:host, ENV['HOSTS']) if ENV['HOSTS']
fh = fetch_for(:filter,{})
@filters << Filter.new(:host, fh[:host]) if fh[:host]
@filters << Filter.new(:role, fh[:role]) if fh[:role]
end
def add_cmdline_filter(type, values)
cmdline_filters << Filter.new(type, values)
end end
def filter list def filter list
external_filters.reduce(list){|l,f| f.filter list} setup_filters if @filters.nil?
@filters.reduce(list) { |l,f| f.filter l }
end end
private private
def cmdline_filters
@cmdline_filters ||= []
end
def servers def servers
@servers ||= Servers.new @servers ||= Servers.new
end end
@ -105,10 +119,6 @@ module Capistrano
@config ||= Hash.new @config ||= Hash.new
end end
def external_filters
@external_filters ||= []
end
def fetch_for(key, default, &block) def fetch_for(key, default, &block)
if block_given? if block_given?
config.fetch(key, &block) config.fetch(key, &block)

View file

@ -18,11 +18,7 @@ module Capistrano
def roles_for(names) def roles_for(names)
options = extract_options(names) options = extract_options(names)
fia = Array(Filter.new(:role, names)) s = Filter.new(:role, names).filter(servers)
fs = Configuration.env.fetch(:filter,{})
fia << Filter.new(:host, fs[:host]) if fs[:host]
fia << Filter.new(:role, fs[:role]) if fs[:role]
s = fia.reduce(servers){|m,o| o.filter m}
s.select { |server| server.select?(options) } s.select { |server| server.select?(options) }
end end

View file

@ -3,6 +3,7 @@ require 'capistrano/dsl/task_enhancements'
require 'capistrano/dsl/paths' require 'capistrano/dsl/paths'
require 'capistrano/dsl/stages' require 'capistrano/dsl/stages'
require 'capistrano/dsl/env' require 'capistrano/dsl/env'
require 'capistrano/configuration/filter'
module Capistrano module Capistrano
module DSL module DSL
@ -49,9 +50,13 @@ module Capistrano
VersionValidator.new(locked_version).verify VersionValidator.new(locked_version).verify
end end
# Having intercepted the SSHKit on() method we can filter externally
def on(hosts, options={}, &block) def on(hosts, options={}, &block)
sshkit_on(Configuration.env.filter(hosts), options, &block) subset = Configuration.env.filter hosts
SSHKit::Coordinator.new(subset).each(options, &block)
end
def run_locally(&block)
SSHKit::Backend::Local.new(&block).run
end end
end end

View file

@ -36,10 +36,10 @@ describe Capistrano::DSL do
end end
end end
context 'with filter options' do context 'with property filter options' do
subject { dsl.release_roles(:all, filter: :active) } subject { dsl.release_roles(:all, filter: :active) }
it 'returns all release servers that match the filter' do it 'returns all release servers that match the property filter' do
expect(subject.map(&:hostname)).to eq %w{example1.com example3.com} expect(subject.map(&:hostname)).to eq %w{example1.com example3.com}
end end
end end
@ -92,7 +92,7 @@ describe Capistrano::DSL do
end end
end end
context 'when the attribute `primary` is explicity set' do context 'when the attribute `primary` is explicitly set' do
subject { dsl.primary(:app) } subject { dsl.primary(:app) }
it 'returns the servers' do it 'returns the servers' do
expect(subject.hostname).to eq 'example4.com' expect(subject.hostname).to eq 'example4.com'
@ -102,35 +102,36 @@ describe Capistrano::DSL do
describe 'setting an internal host filter' do describe 'setting an internal host filter' do
subject { dsl.roles(:app) } subject { dsl.roles(:app) }
it 'returns one' do it 'is ignored' do
dsl.set :filter, { host: 'example3.com' } dsl.set :filter, { host: 'example3.com' }
expect(subject.map(&:hostname)).to eq(['example3.com']) expect(subject.map(&:hostname)).to eq(['example3.com', 'example4.com'])
end end
end end
describe 'setting an internal role filter' do describe 'setting an internal role filter' do
subject { dsl.roles(:app) } subject { dsl.roles(:app) }
it 'returns one' do it 'ignores it' do
dsl.set :filter, { role: :web } dsl.set :filter, { role: :web }
expect(subject.map(&:hostname)).to eq(['example3.com']) expect(subject.map(&:hostname)).to eq(['example3.com','example4.com'])
end end
end end
describe 'setting an internal host and role filter' do describe 'setting an internal host and role filter' do
subject { dsl.roles(:app) } subject { dsl.roles(:app) }
it 'returns one' do it 'ignores it' do
dsl.set :filter, { role: :web, host: 'example1.com' } dsl.set :filter, { role: :web, host: 'example1.com' }
expect(subject.map(&:hostname)).to be_empty expect(subject.map(&:hostname)).to eq(['example3.com','example4.com'])
end end
end end
describe 'setting an internal regexp host filter' do describe 'setting an internal regexp host filter' do
subject { dsl.roles(:all) } subject { dsl.roles(:all) }
it 'works' do it 'is ignored' do
dsl.set :filter, { host: /1/ } dsl.set :filter, { host: /1/ }
expect(subject.map(&:hostname)).to eq(['example1.com']) expect(subject.map(&:hostname)).to eq(%w{example1.com example2.com example3.com example4.com example5.com})
end end
end end
end end
describe 'when defining role with reserved name' do describe 'when defining role with reserved name' do
@ -549,4 +550,61 @@ describe Capistrano::DSL do
end end
end end
end end
describe 'on()' do
before do
dsl.server 'example1.com', roles: %w{web}, active: true
dsl.server 'example2.com', roles: %w{web}
dsl.server 'example3.com', roles: %w{app web}, active: true
dsl.server 'example4.com', roles: %w{app}, primary: true
dsl.server 'example5.com', roles: %w{db}, no_release: true
@coordinator = mock('coordinator')
@coordinator.expects(:each).returns(nil)
ENV.delete 'ROLES'
ENV.delete 'HOSTS'
end
it 'filters by role from the :filter variable' do
hosts = dsl.roles(:web)
all = dsl.roles(:all)
SSHKit::Coordinator.expects(:new).with(hosts).returns(@coordinator)
dsl.set :filter, { role: 'web' }
dsl.on(all)
end
it 'filters by host and role from the :filter variable' do
all = dsl.roles(:all)
SSHKit::Coordinator.expects(:new).with([]).returns(@coordinator)
dsl.set :filter, { role: 'db', host: 'example3.com' }
dsl.on(all)
end
it 'filters from ENV[ROLES]' do
hosts = dsl.roles(:db)
all = dsl.roles(:all)
SSHKit::Coordinator.expects(:new).with(hosts).returns(@coordinator)
ENV['ROLES'] = 'db'
dsl.on(all)
end
it 'filters from ENV[HOSTS]' do
hosts = dsl.roles(:db)
all = dsl.roles(:all)
SSHKit::Coordinator.expects(:new).with(hosts).returns(@coordinator)
ENV['HOSTS'] = 'example5.com'
dsl.on(all)
end
it 'filters by ENV[HOSTS] && ENV[ROLES]' do
all = dsl.roles(:all)
SSHKit::Coordinator.expects(:new).with([]).returns(@coordinator)
ENV['HOSTS'] = 'example5.com'
ENV['ROLES'] = 'web'
dsl.on(all)
end
end
end end

View file

@ -82,11 +82,11 @@ module Capistrano
expect(servers.fetch_primary(:app).hostname).to eq('2') expect(servers.fetch_primary(:app).hostname).to eq('2')
end end
it 'honours any internal filters' do it 'ignores any on_filters' do
Configuration.env.set :filter, { host: '1'} Configuration.env.set :filter, { host: '1'}
servers.add_role(:app, %w{1 2}) servers.add_role(:app, %w{1 2})
servers.add_host('2', primary: true) servers.add_host('2', primary: true)
expect(servers.fetch_primary(:app).hostname).to eq('1') expect(servers.fetch_primary(:app).hostname).to eq('2')
end end
end end