From a1d3bfb524102b92a2f6a9f875fe588ee1efa8e0 Mon Sep 17 00:00:00 2001 From: Andy Sykes Date: Tue, 26 Nov 2013 14:51:13 +0000 Subject: [PATCH] Add ability to filter tasks to specific servers (host filtering). This commit adds the ability to control what servers are involved in an task from the command line, or by setting an environment variable. The filter contains a list of servers; these are the only servers that will be involved in the task. For example, if you had three servers defined in your configuration (server1, server2 and server3), you could deploy to only server1 by doing either: cap --hosts server1 production deploy Or: HOSTS=server1 cap production deploy Multiple servers can be specified by separating them with commas: cap --hosts server1,server2 production deploy HOSTS=server1,server2 cap production deploy Host filtering happens after role filtering, and does not change what roles a server will respond to. --- CHANGELOG.md | 1 + lib/capistrano/application.rb | 12 ++- lib/capistrano/configuration/servers.rb | 3 +- .../configuration/servers/host_filter.rb | 82 ++++++++++++++++++ .../configuration/servers/host_filter_spec.rb | 86 +++++++++++++++++++ .../capistrano/configuration/servers_spec.rb | 1 + 6 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 lib/capistrano/configuration/servers/host_filter.rb create mode 100644 spec/lib/capistrano/configuration/servers/host_filter_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7efda0..7541d37e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Reverse Chronological Order: ## master + * Add ability to filter tasks to specific servers (host filtering). (@andytinycat) * Add a command line option to control role filter (`--roles`) (@andytinycat) ## `3.1.0` (not released) diff --git a/lib/capistrano/application.rb b/lib/capistrano/application.rb index 7ebf5034..14d3c7c8 100644 --- a/lib/capistrano/application.rb +++ b/lib/capistrano/application.rb @@ -16,7 +16,7 @@ module Capistrano end def sort_options(options) - options.push(version, dry_run, roles) + options.push(version, roles, dry_run, hostfilter) super end @@ -74,6 +74,16 @@ module Capistrano } ] end + + def hostfilter + ['--hosts HOSTS', '-z', + "Filter command to only apply to these hosts (separate multiple hosts with a comma)", + lambda { |value| + Configuration.env.set(:filter, :hosts => value.split(",")) + } + ] + end + end end diff --git a/lib/capistrano/configuration/servers.rb b/lib/capistrano/configuration/servers.rb index d52e65ef..54f8d4a7 100644 --- a/lib/capistrano/configuration/servers.rb +++ b/lib/capistrano/configuration/servers.rb @@ -1,5 +1,6 @@ require 'set' require_relative 'servers/role_filter' +require_relative 'servers/host_filter' module Capistrano class Configuration class Servers @@ -39,7 +40,7 @@ module Capistrano def fetch_roles(required, options) filter_roles = RoleFilter.for(required, available_roles) - select(servers_with_roles(filter_roles), options) + HostFilter.for(select(servers_with_roles(filter_roles), options)) end def servers_with_roles(roles) diff --git a/lib/capistrano/configuration/servers/host_filter.rb b/lib/capistrano/configuration/servers/host_filter.rb new file mode 100644 index 00000000..568cc6b7 --- /dev/null +++ b/lib/capistrano/configuration/servers/host_filter.rb @@ -0,0 +1,82 @@ +module Capistrano + class Configuration + class Servers + class HostFilter + + def initialize(available) + @available = available + end + + def self.for(available) + new(available).hosts + end + + def hosts + if host_filter.any? + @available.select { |server| host_filter.include? server.hostname } + else + @available + end + end + + private + + def filter + if host_filter.any? + host_filter + else + @available + end + end + + def host_filter + env_filter | configuration_filter + end + + def configuration_filter + ConfigurationFilter.new.hosts + end + + def env_filter + EnvFilter.new.hosts + end + + class ConfigurationFilter + + def hosts + if filter + Array(filter.fetch(:hosts, [])) + else + [] + end + end + + def config + Configuration.env + end + + def filter + config.fetch(:filter) || config.fetch(:select) + end + end + + + class EnvFilter + + def hosts + if filter + filter.split(',') + else + [] + end + end + + def filter + ENV['HOSTS'] + end + end + + end + end + end +end diff --git a/spec/lib/capistrano/configuration/servers/host_filter_spec.rb b/spec/lib/capistrano/configuration/servers/host_filter_spec.rb new file mode 100644 index 00000000..c9e3a82e --- /dev/null +++ b/spec/lib/capistrano/configuration/servers/host_filter_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +module Capistrano + class Configuration + class Servers + + describe HostFilter do + let(:host_filter) { HostFilter.new(available) } + let(:available) { [ Server.new('server1'), Server.new('server2'), Server.new('server3') ] } + + describe '#new' do + it 'takes one array of hostnames' do + expect(host_filter) + end + end + + describe '.for' do + + subject { HostFilter.for(available) } + + context 'without env vars' do + + it 'returns all available hosts' do + expect(subject).to eq available + end + + end + + context 'with ENV vars' do + before do + ENV.stubs(:[]).with('HOSTS').returns('server1,server2') + end + + it 'returns all required hosts defined in HOSTS' do + expect(subject).to eq [Server.new('server1'), Server.new('server2')] + end + end + + context 'with configuration filters' do + before do + Configuration.env.set(:filter, hosts: %w{server1 server2}) + end + + it 'returns all required hosts defined in the filter' do + expect(subject).to eq [Server.new('server1'), Server.new('server2')] + end + + after do + Configuration.env.delete(:filter) + end + end + + context 'with a single configuration filter' do + before do + Configuration.env.set(:filter, hosts: 'server3') + end + + it 'returns all required hosts defined in the filter' do + expect(subject).to eq [Server.new('server3')] + end + + after do + Configuration.env.delete(:filter) + end + end + + context 'with configuration filters and ENV vars' do + before do + Configuration.env.set(:filter, hosts: 'server1') + ENV.stubs(:[]).with('HOSTS').returns('server3') + end + + it 'returns all required hosts defined in the filter' do + expect(subject).to eq [Server.new('server1'), Server.new('server3')] + end + + after do + Configuration.env.delete(:filter) + end + end + end + end + + end + end +end diff --git a/spec/lib/capistrano/configuration/servers_spec.rb b/spec/lib/capistrano/configuration/servers_spec.rb index 6b659c51..c97eb25f 100644 --- a/spec/lib/capistrano/configuration/servers_spec.rb +++ b/spec/lib/capistrano/configuration/servers_spec.rb @@ -179,6 +179,7 @@ module Capistrano before do ENV.stubs(:[]).with('ROLES').returns('web,db') + ENV.stubs(:[]).with('HOSTS').returns(nil) servers.add_host('1', roles: :app, active: true) servers.add_host('2', roles: :app) servers.add_host('3', roles: :web)