hubcap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Capfile.example ADDED
@@ -0,0 +1,37 @@
1
+ # This just lets us test Hubcap without installing it as a gem...
2
+ #
3
+ $LOAD_PATH.unshift('lib')
4
+
5
+ # Force Hubcap into agnostic mode with AGNOSTIC=1, or application mode with 0.
6
+ # Use this environment variable with care!
7
+ #
8
+ # if ag = ENV['AGNOSTIC']
9
+ # set(:hubcap_agnostic, ag == '1') if ['0', '1'].include?(ag)
10
+ # end
11
+
12
+
13
+ # SSH user (and login password if required).
14
+ #
15
+ set(:user, ENV['AS'] || 'deploy')
16
+ set(:password) {
17
+ puts
18
+ warn("A server is prompting for the \"#{user}\" user's login password.")
19
+ warn("NB: You can run as another user with the AS environment variable.")
20
+ Capistrano::CLI.password_prompt
21
+ }
22
+
23
+
24
+ # This is only processed if we are using cap directly (not bin/hubcap).
25
+ #
26
+ # Load servers and sets from node config. Any recipes loaded after this
27
+ # point will be available only in application mode.
28
+ #
29
+ unless exists?(:hubcap)
30
+ if (target = ENV['TO']) && !ENV['TO'].empty?
31
+ target = '' if target == 'ALL'
32
+ require('hubcap')
33
+ Hubcap.load(target, 'test/data').configure_capistrano(self)
34
+ else
35
+ warn("NB: No servers specified. Target a Hubcap group or server with TO.")
36
+ end
37
+ end
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # Hubcap
2
+
3
+ Create a hub for your server configuration. Use it with Capistrano,
4
+ Puppet and others.
5
+
6
+
7
+ ## Meet Hubcap
8
+
9
+ You want to provision your servers with Puppet. You want to deploy to your
10
+ servers with Capistrano. Where do you define your server infrastructure?
11
+
12
+ Hubcap lets you define the characteristics of your servers once. Then, when you
13
+ need to use Puppet, Capistrano drives. It deploys your Puppet modules and
14
+ manifests, plus a special host-specific file, and applies it to the server.
15
+ (This is sometimes called "masterless Puppet". It has a lot of benefits that
16
+ derive from decentralization and pushing changes on-demand.)
17
+
18
+ Here's what your config file might look like:
19
+
20
+ # An application called 'readme' that uses Cap's default deployment recipe.
21
+ application('readme', :recipes => 'deploy') {
22
+ # Set a capistrano variable.
23
+ cap_set('repository', 'git@github.com:joseph/readme.git')
24
+
25
+ # Declare that all servers will have the 'baseline' puppet class.
26
+ role(:puppet => 'baseline')
27
+
28
+ group('staging') {
29
+ # Puppet gets a $::exception_subject_prefix variable on these servers.
30
+ param('exception_subject_prefix' => '[STAGING] ')
31
+ # For simple staging, just one server that does everything.
32
+ server('readme.stage') {
33
+ role(:cap => [:web, :app, :db], :puppet => ['proxy', 'app', 'db'])
34
+ }
35
+ }
36
+
37
+ group('production') {
38
+ # Puppet gets these top-scope variables on servers in this group.
39
+ param(
40
+ 'exception_subject_prefix' => '[PRODUCTION] ',
41
+ 'env' => {
42
+ "FORCE_SSL" => true,
43
+ "S3_KEY" => "AKIAKJRK23943202JK",
44
+ "S3_SECRET" => "KDJkaddsalkjfkawjri32jkjaklvjgakljkj"
45
+ }
46
+ )
47
+
48
+ group('proxy') {
49
+ # Servers will have the :web role and the 'proxy' puppet class.
50
+ role(:cap => :web, :puppet => 'proxy')
51
+ server('proxy-1', :address => '10.10.10.5')
52
+ }
53
+
54
+ group('app') {
55
+ # Servers will have the :app role and the 'app' puppet class.
56
+ role(:app)
57
+ server('app-1', :address => '10.10.10.10')
58
+ server('app-2', :address => '10.10.10.11')
59
+ }
60
+
61
+ group('db') {
62
+ role(:db)
63
+ server('db-1', :address => '10.10.10.50')
64
+ }
65
+ }
66
+ }
67
+
68
+
69
+ Save this as `hub/example.rb`.
70
+
71
+ Run:
72
+
73
+ $ hubcap ALL servers:tree
74
+
75
+ That's a lot of info. You can filter your server list to target specific
76
+ groups of servers:
77
+
78
+ $ `hubcap example.vagrant servers:tree`
79
+
80
+ You can run `list` in place of `tree` to see just the servers that match
81
+ your filter:
82
+
83
+ $ `hubcap example.production.db servers:tree`
84
+
85
+
86
+ ## Working with Puppet
87
+
88
+ You should have your Puppet modules in a git repository. The location of this
89
+ repository should be specified in your Capfile with
90
+ `set(:puppet_repository, '...')`. Your site manifest should be within this repo
91
+ at `puppet/host.pp` (but this is also configurable).
92
+
93
+ When you're ready to provision some servers:
94
+
95
+ $ `hubcap example.vagrant puppet:noop`
96
+ $ `hubcap example.vagrant puppet:apply`
97
+
98
+ Once that's done, you can deploy your app in the usual way:
99
+
100
+ $ `hubcap example.vagrant deploy:setup deploy:cold`
101
+
102
+
103
+
104
+ ## The Hubcap DSL
105
+
106
+ The Hubcap DSL is very simple. This is the basic set of statements:
107
+
108
+ * `group` - A named set of servers, roles, variables, attributes. Groups
109
+ can be nested.
110
+
111
+ * `application` - A special kind of group. You can pass `:recipes => ...`
112
+ to this declaration. Each recipe path will be loaded into Capistrano only
113
+ for this application. Applications can't be nested.
114
+
115
+ * `server` - An actual host that you are managing with Capistrano and
116
+ Puppet. The first argument is the name, which can be an IP address or domain
117
+ name if you like. Otherwise, pass `:address => '...'`.
118
+
119
+ * `cap_set` - Set a Capistrano variable.
120
+
121
+ * `cap_attribute` - Set a Cap attribute on all the servers within this
122
+ group, such as `:primary => true` or `:no_release => true`.
123
+
124
+ * `role` - Add a role to the list of Capistrano roles for servers within
125
+ this group. By default, these roles are supplied as classes to apply to the
126
+ host in Puppet. You can specify that a role is Capistrano-only with
127
+ `:cap => '...'`, or Puppet-only with :puppet => `'...'`. This is additive:
128
+ if you have multiple role declarations in your tree, all of them apply.
129
+
130
+ * `param` - Add to a hash of 'parameters' that will be supplied to Puppet
131
+ as top-scope variables for servers in this group. Like `role`, this is
132
+ additive.
133
+
134
+ Hubcap uses Puppet's External Node Classifier (ENC) feature to provide the
135
+ list of classes and parameters for a specific host. More info here:
136
+ http://docs.puppetlabs.com/guides/external_nodes.html
137
+
138
+
139
+ ## Hubcap as a library
140
+
141
+ If you'd rather run `cap` than `hubcap`, you can load your hub configuration
142
+ directly in your `Capfile`. Add this to the end of the file:
143
+
144
+ require('hubcap')
145
+ Hubcap.load('', 'hub').configure_capistrano(self)
146
+
147
+ The two arguments to `Hubcap.load` are the filter (where `''` means no filter),
148
+ and the path to the hub configuration. This will load `*.rb` in the `hub`
149
+ directory (but not subdirectories). You can specify multiple paths as additional
150
+ arguments -- whole directories or specific files.
151
+
152
+ If you want to simulate the behaviour of the `hubcap` script, you could do it
153
+ with something like this in your `Capfile`.
154
+
155
+ # Load servers and sets from node config. Any recipes loaded after this
156
+ # point will be available only in application mode.
157
+ if (target = ENV['TO']) && !ENV['TO'].empty?
158
+ target = '' if target == 'ALL'
159
+ require('hubcap')
160
+ Hubcap.load(target, 'hub').configure_capistrano(self)
161
+ else
162
+ warn("NB: No servers specified. Target a Hubcap group with TO.")
163
+ end
164
+
165
+ In this set-up, you'd run `cap` like this:
166
+
167
+ $ cap TO=example.vagrant servers:tree
168
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new { |t|
4
+ t.libs << 'test'
5
+ t.test_files = FileList['test/unit/test*.rb']
6
+ t.verbose = true
7
+ }
8
+
9
+ desc("Run tests")
10
+ task(:default => :test)
data/bin/hubcap ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # 'hubcap' - a convenience script for loading Capistrano with Hubcap config.
4
+ #
5
+ # Usages:
6
+ #
7
+ # # This will list the available default tasks:
8
+ # hubcap -T
9
+ #
10
+ # # This task (or tasks) will run on all defined servers in the hub:
11
+ # hubcap ALL task:name ...
12
+ #
13
+ # # This task will run on all servers inside the specified group:
14
+ # hubcap app_name.group_name task:name ...
15
+ #
16
+ # # This will run on just the specified server:
17
+ # hubcap app_name.group_name.server_name task:name ...
18
+ #
19
+ #
20
+ # Note that the hub configuration files are loaded from a subdirectory of the
21
+ # current directory named 'hub'. You can change this to point at another
22
+ # directory, single file or multiple files by setting the HUB_CONFIG env var.
23
+ #
24
+ # HUB_CONFIG=test/data hubcap ALL servers:list
25
+ #
26
+ # HUB_CONFIG=test/data/example.rb,test/data/simple.rb hubcap ALL servers:tree
27
+ #
28
+ #
29
+
30
+
31
+ require('capistrano/cli')
32
+ require('hubcap')
33
+
34
+
35
+ class Hubcap::CLI < Capistrano::CLI
36
+
37
+ attr_accessor(:cap)
38
+
39
+
40
+ def self.roll!
41
+ target = pre_parse_for_target(ARGV)
42
+
43
+ if !target
44
+ puts("Usage: hubcap name.of.target.group task:name")
45
+ puts("To target all servers: hubcap ALL task:name")
46
+ exit
47
+ end
48
+
49
+ if target == :skip
50
+ parse(ARGV).execute!
51
+ else
52
+ filter = (target == 'ALL') ? '' : target
53
+ paths = ['hub']
54
+ paths = ENV['HUB_CONFIG'].split(',') if ENV['HUB_CONFIG']
55
+ hub = Hubcap.load(filter, *paths)
56
+ unless hub.children.any?
57
+ puts("Hubcap error: no servers for '#{target}' in [#{paths.join(',')}]")
58
+ exit
59
+ end
60
+ parse(ARGV) { |cap|
61
+ cap.load('standard')
62
+ hub.configure_capistrano(cap)
63
+ }.execute!
64
+ end
65
+ end
66
+
67
+
68
+ def self.pre_parse_for_target(args)
69
+ if args.length == 1 && args.first.match(/^-/)
70
+ return :skip
71
+ elsif args.length < 1
72
+ puts("Error: no servers specified")
73
+ return nil
74
+ elsif args.length < 2
75
+ puts("Error: no tasks specified")
76
+ return nil
77
+ end
78
+ return args.shift
79
+ end
80
+
81
+
82
+ def self.parse(args)
83
+ cli = new(args)
84
+ cli.parse_options!
85
+ cli.cap = Capistrano::Configuration.new(cli.options)
86
+ yield(cli.cap) if block_given?
87
+ cli
88
+ end
89
+
90
+
91
+ def instantiate_configuration(options = {})
92
+ self.cap
93
+ end
94
+
95
+ end
96
+
97
+
98
+ Hubcap::CLI.roll!
data/hubcap.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/hubcap/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ['Joseph Pearson']
6
+ gem.email = ['joseph@booki.sh']
7
+ gem.description = 'Unite Capistrano and Puppet config in one Ruby file.'
8
+ gem.summary = 'Hubcap Capistrano/Puppet extension'
9
+ gem.homepage = ''
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = 'hubcap'
15
+ gem.require_paths = ['lib']
16
+ gem.version = Hubcap::VERSION
17
+ end
@@ -0,0 +1,24 @@
1
+ class Hubcap::Application < Hubcap::Group
2
+
3
+ attr_reader(:recipe_paths)
4
+
5
+ def initialize(parent, name, options = {}, &blk)
6
+ @recipe_paths = [options[:recipes]].flatten.compact
7
+ super(parent, name, &blk)
8
+ end
9
+
10
+
11
+ def application(*args)
12
+ raise(Hubcap::NestedApplicationDisallowed)
13
+ end
14
+
15
+
16
+ def extend_tree(outs)
17
+ outs << "Load: #{@recipe_paths.inspect}" if @recipe_paths.any?
18
+ end
19
+
20
+
21
+ class Hubcap::NestedApplicationDisallowed < StandardError; end
22
+
23
+ end
24
+
@@ -0,0 +1,223 @@
1
+ class Hubcap::Group
2
+
3
+ attr_reader(
4
+ :name,
5
+ :cap_attributes,
6
+ :cap_roles,
7
+ :puppet_roles,
8
+ :params,
9
+ :parent,
10
+ :children
11
+ )
12
+
13
+ # Supply the parent group, the name of this new group and a block of code to
14
+ # evaluate in the context of this new group.
15
+ #
16
+ # Every group must have a parent group, unless it is the top-most group: the
17
+ # hub. The hub must be a Hubcap::Hub.
18
+ #
19
+ def initialize(parent, name, &blk)
20
+ @name = name.to_s
21
+ if @parent = parent
22
+ @cap_attributes = parent.cap_attributes.clone
23
+ @cap_roles = parent.cap_roles.clone
24
+ @puppet_roles = parent.puppet_roles.clone
25
+ @params = parent.params.clone
26
+ elsif !kind_of?(Hubcap::Hub)
27
+ raise(Hubcap::GroupWithoutParent, self.inspect)
28
+ end
29
+ @children = []
30
+ instance_eval(&blk) if blk && processable?
31
+ end
32
+
33
+
34
+ # Load a Ruby file and evaluate it in the context of this group.
35
+ # Like Ruby's require(), the '.rb' is optional in the path.
36
+ #
37
+ def absorb(path)
38
+ p = path
39
+ p += '.rb' unless File.exists?(p)
40
+ raise("File not found: #{path}") unless File.exists?(p)
41
+ code = IO.read(p)
42
+ eval(code)
43
+ end
44
+
45
+
46
+ # Finds the top-level Hubcap::Hub to which this group belongs.
47
+ #
48
+ def hub
49
+ @parent ? @parent.hub : self
50
+ end
51
+
52
+
53
+ # An array of names, from the oldest ancestor to the parent to self.
54
+ #
55
+ def history
56
+ @parent ? @parent.history + [@name] : []
57
+ end
58
+
59
+
60
+ # Indicates whether we should process this group. We process all groups that
61
+ # match the filter or are below the furthest point in the filter.
62
+ #
63
+ # "Match the filter" means that this group's history and the filter are
64
+ # identical to the end of the shortest of the two arrays.
65
+ #
66
+ def processable?
67
+ s = [history.length, hub.filter.length].min
68
+ history.slice(0,s) == hub.filter.slice(0,s)
69
+ end
70
+
71
+
72
+ # Indicates whether we should store this group in the hub's array of
73
+ # groups/applications/servers. We only store groups at the end of the filter
74
+ # and below.
75
+ #
76
+ # That is, this group's history should be the same length or longer than the
77
+ # filter, but identical at each point in the filter.
78
+ #
79
+ def collectable?
80
+ (history.length >= hub.filter.length) && processable?
81
+ end
82
+
83
+
84
+ # Sets a variable in the Capistrano instance.
85
+ #
86
+ # Note: when Hubcap is in application mode (not executing a default task),
87
+ # an exception will be raised if a variable is set twice to two different
88
+ # values.
89
+ #
90
+ # Either:
91
+ # cap_set(:foo, 'bar')
92
+ # or:
93
+ # cap_set(:foo => 'bar')
94
+ # and this works too:
95
+ # cap_set(:foo => 'bar', :garply => 'grault')
96
+ # in fact, even this works:
97
+ # cap_set(:foo) { bar }
98
+ #
99
+ def cap_set(*args, &blk)
100
+ if args.length == 2
101
+ hub.cap_set(args.first => args.last)
102
+ elsif args.length == 1
103
+ if block_given?
104
+ hub.cap_set(args.first => blk)
105
+ elsif args.first.kind_of?(Hash)
106
+ hub.cap_set(args.first)
107
+ end
108
+ else
109
+ raise ArgumentError('Must be (key, value) or (hash) or (key) { block }.')
110
+ end
111
+ end
112
+
113
+
114
+ # Sets an attribute in the Capistrano server() definition for all Hubcap
115
+ # servers to which it applies.
116
+ #
117
+ # For eg, :primary => true or :no_release => true.
118
+ #
119
+ # Either:
120
+ # cap_attribute(:foo, 'bar')
121
+ # or:
122
+ # cap_attribute(:foo => 'bar')
123
+ # and this works too:
124
+ # cap_attribute(:foo => 'bar', :garply => 'grault')
125
+ #
126
+ def cap_attribute(*args)
127
+ if args.length == 2
128
+ cap_attribute(args.first => args.last)
129
+ elsif args.length == 1 && args.first.kind_of?(Hash)
130
+ @cap_attributes.update(args.first)
131
+ else
132
+ raise ArgumentError('Must be (key, value) or (hash).')
133
+ end
134
+ end
135
+
136
+
137
+ # Sets the Capistrano role and/or Puppet class for all Hubcap servers to
138
+ # which it applies.
139
+ #
140
+ # When declared multiple times (even in parents), it's additive.
141
+ #
142
+ # Either:
143
+ # role(:app)
144
+ # or:
145
+ # role(:app, :db)
146
+ # or:
147
+ # role(:cap => :app, :puppet => 'relishapp')
148
+ # or:
149
+ # role(:cap => [:app, :db], :puppet => 'relishapp')
150
+ #
151
+ def role(*args)
152
+ if args.length == 1 && args.first.kind_of?(Hash)
153
+ h = args.first
154
+ @cap_roles += [h[:cap]].flatten if h.has_key?(:cap)
155
+ @puppet_roles += [h[:puppet]].flatten if h.has_key?(:puppet)
156
+ else
157
+ @cap_roles += args
158
+ @puppet_roles += args
159
+ end
160
+ end
161
+
162
+
163
+ # Adds values to a hash that is supplied to Puppet when it is provisioning
164
+ # the server.
165
+ #
166
+ # If you do this...
167
+ # params(:foo => 'bar')
168
+ # ...then Puppet will have a top-level variable called $foo, containing 'bar'.
169
+ #
170
+ def param(hash)
171
+ @params.update(hash)
172
+ end
173
+
174
+
175
+ # Instantiate an application as a child of this group.
176
+ #
177
+ def application(name, options = {}, &blk)
178
+ add_child(:applications, Hubcap::Application.new(self, name, options, &blk))
179
+ end
180
+
181
+
182
+ # Instantiate a server as a child of this group.
183
+ #
184
+ def server(name, options = {}, &blk)
185
+ add_child(:servers, Hubcap::Server.new(self, name, options, &blk))
186
+ end
187
+
188
+
189
+ # Instantiate a group as a child of this group.
190
+ #
191
+ def group(name, &blk)
192
+ add_child(:groups, Hubcap::Group.new(self, name, &blk))
193
+ end
194
+
195
+
196
+ # Returns a formatted string of all the key details for this group, and
197
+ # recurses into each child.
198
+ #
199
+ def tree(indent = " ")
200
+ outs = [self.class.name.split('::').last.upcase, "Name: #{@name}"]
201
+ outs << "Atts: #{@cap_attributes.inspect}" if @cap_attributes.any?
202
+ outs << "Pram: #{@params.inspect}" if @params.any?
203
+ extend_tree(outs) if respond_to?(:extend_tree)
204
+ outs << ""
205
+ if @children.any?
206
+ @children.each { |child| outs << child.tree(indent+" ") }
207
+ end
208
+ outs.join("\n#{indent}")
209
+ end
210
+
211
+
212
+ private
213
+
214
+ def add_child(category, child)
215
+ @children << child if child.processable?
216
+ hub.send(category) << child if child.collectable?
217
+ child
218
+ end
219
+
220
+
221
+ class Hubcap::GroupWithoutParent < StandardError; end
222
+
223
+ end
data/lib/hubcap/hub.rb ADDED
@@ -0,0 +1,125 @@
1
+ class Hubcap::Hub < Hubcap::Group
2
+
3
+ attr_reader(:filter, :applications, :servers, :groups, :cap_sets)
4
+
5
+
6
+ def initialize(filter_string)
7
+ @filter = filter_string.split('.')
8
+ @cap_sets = {}
9
+ @cap_set_clashes = []
10
+ @cap_attributes = {}
11
+ @cap_roles = []
12
+ @puppet_roles = []
13
+ @params = {}
14
+ @applications = []
15
+ @servers = []
16
+ @groups = []
17
+ super(nil, '∞')
18
+ end
19
+
20
+
21
+ def cap_set(hash)
22
+ hash.each_pair { |k, v|
23
+ if @cap_sets[k] && @cap_sets[k] != v
24
+ @cap_set_clashes << { k => v }
25
+ else
26
+ @cap_sets[k] = v
27
+ end
28
+ }
29
+ end
30
+
31
+
32
+ def extend_tree(outs)
33
+ outs << "Sets: #{@cap_sets.inspect}"
34
+ end
35
+
36
+
37
+ # Does a few things:
38
+ #
39
+ # * Sets the :hubcap variable in the Capistrano instance.
40
+ # * Loads the default Hubcap recipes into the Capistrano instance.
41
+ # * Defines each server as a Capistrano server().
42
+ #
43
+ # If we are in "agnostic mode" - executing a default task - that's all we
44
+ # do. If we are in "application mode" - executing at least one non-standard
45
+ # task - then we do a few more things:
46
+ #
47
+ # * Load all the recipes for the application into the Capistrano instance.
48
+ # * Set all the @cap_set variables in the Capistrano instance.
49
+ #
50
+ def configure_capistrano(cap)
51
+ raise(Hubcap::CapistranoAlreadyConfigured) if cap.exists?(:hubcap)
52
+ cap.set(:hubcap, self)
53
+
54
+ cap.instance_eval {
55
+ require('hubcap/recipes/servers')
56
+ require('hubcap/recipes/puppet')
57
+ }
58
+
59
+ # Declare the servers.
60
+ servers.each { |s|
61
+ cap.server(s.address, *(s.cap_roles + [s.cap_attributes]))
62
+ }
63
+
64
+ configure_application_mode(cap) unless capistrano_is_agnostic?(cap)
65
+ end
66
+
67
+
68
+ # In agnostic mode, Capistrano recipes for specific applications are not
69
+ # loaded, and cap_set collisions are ignored.
70
+ #
71
+ def capistrano_is_agnostic?(cap)
72
+ return cap.fetch(:hubcap_agnostic) if cap.exists?(:hubcap_agnostic)
73
+ ag = true
74
+ options = cap.logger.instance_variable_get(:@options)
75
+ if options && options[:actions] && options[:actions].any?
76
+ tasks = options[:actions].clone
77
+ while tasks.any?
78
+ ag = false unless cap.find_task(tasks.shift)
79
+ end
80
+ end
81
+ cap.set(:hubcap_agnostic, ag)
82
+ ag
83
+ end
84
+
85
+
86
+ private
87
+
88
+ def configure_application_mode(cap)
89
+ apps = servers.collect(&:application_parent).compact.uniq
90
+
91
+ # A - there should be only one application for all the servers
92
+ raise(
93
+ Hubcap::ApplicationModeError::TooManyApplications,
94
+ apps.collect(&:name).join(', ')
95
+ ) if apps.size > 1
96
+
97
+ # B - there should be no clash of cap sets
98
+ raise(
99
+ Hubcap::ApplicationModeError::DuplicateSets,
100
+ @cap_set_clashes.inspect
101
+ ) if @cap_set_clashes.any?
102
+
103
+ # C - app-specific, but no applications
104
+ raise(Hubcap::ApplicationModeError::NoApplications) if !apps.any?
105
+
106
+ # Otherwise, load all recipes...
107
+ cap.set(:application, apps.first.name)
108
+ apps.first.recipe_paths.each { |rp| cap.load(rp) }
109
+
110
+ # ..and declare all cap sets.
111
+ @cap_sets.each_pair { |key, val|
112
+ val.kind_of?(Proc) ? cap.set(key, &val) : cap.set(key, val)
113
+ }
114
+ end
115
+
116
+
117
+
118
+ class Hubcap::CapistranoAlreadyConfigured < StandardError; end
119
+ class Hubcap::ApplicationModeError < StandardError;
120
+ class TooManyApplications < self; end
121
+ class NoApplications < self; end
122
+ class DuplicateSets < self; end
123
+ end
124
+
125
+ end