hubcap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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