ridley 0.7.0.rc4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ridley.rb CHANGED
@@ -28,8 +28,10 @@ module Ridley
28
28
  autoload :Client, 'ridley/client'
29
29
  autoload :Connection, 'ridley/connection'
30
30
  autoload :ChainLink, 'ridley/chain_link'
31
+ autoload :Chef, 'ridley/chef'
31
32
  autoload :DSL, 'ridley/dsl'
32
33
  autoload :Logging, 'ridley/logging'
34
+ autoload :Mixin, 'ridley/mixin'
33
35
  autoload :Resource, 'ridley/resource'
34
36
  autoload :SandboxUploader, 'ridley/sandbox_uploader'
35
37
  autoload :SSH, 'ridley/ssh'
@@ -0,0 +1,10 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ #
4
+ # Classes and modules used for integrating with a Chef Server, the Chef community
5
+ # site, and Chef Cookbooks
6
+ module Chef
7
+ autoload :Cookbook, 'ridley/chef/cookbook'
8
+ autoload :Digester, 'ridley/chef/digester'
9
+ end
10
+ end
@@ -0,0 +1,254 @@
1
+ module Ridley::Chef
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class Cookbook
4
+ autoload :Metadata, 'ridley/chef/cookbook/metadata'
5
+ autoload :SyntaxCheck, 'ridley/chef/cookbook/syntax_check'
6
+
7
+ class << self
8
+ # @param [String] filepath
9
+ # a path on disk to the location of a file to checksum
10
+ #
11
+ # @return [String]
12
+ # a checksum that can be used to uniquely identify the file understood
13
+ # by a Chef Server.
14
+ def checksum(filepath)
15
+ Ridley::Chef::Digester.md5_checksum_for_file(filepath)
16
+ end
17
+
18
+ # Creates a new instance of Ridley::Chef::Cookbook from a path on disk containing
19
+ # a Cookbook.
20
+ #
21
+ # The name of the Cookbook is determined by the value of the name attribute set in
22
+ # the cookbooks' metadata. If the name attribute is not present the name of the loaded
23
+ # cookbook is determined by directory containing the cookbook.
24
+ #
25
+ # @param [#to_s] path
26
+ # a path on disk to the location of a Cookbook
27
+ #
28
+ # @option options [String] :name
29
+ # explicitly supply the name of the cookbook we are loading. This is useful if
30
+ # you are dealing with a cookbook that does not have well-formed metadata
31
+ #
32
+ # @raise [IOError] if the path does not contain a metadata.rb file
33
+ #
34
+ # @return [Ridley::Chef::Cookbook]
35
+ def from_path(path, options = {})
36
+ path = Pathname.new(path)
37
+ metadata = Cookbook::Metadata.from_file(path.join('metadata.rb'))
38
+
39
+ name = if options[:name].present?
40
+ options[:name]
41
+ else
42
+ metadata.name.empty? ? File.basename(path) : metadata.name
43
+ end
44
+
45
+ new(name, path, metadata)
46
+ end
47
+ end
48
+
49
+ CHEF_TYPE = "cookbook_version".freeze
50
+ CHEF_JSON_CLASS = "Chef::CookbookVersion".freeze
51
+
52
+ extend Forwardable
53
+
54
+ attr_reader :cookbook_name
55
+ attr_reader :path
56
+ attr_reader :metadata
57
+ # @return [Hashie::Mash]
58
+ # a Hashie::Mash containing Cookbook file category names as keys and an Array of Hashes
59
+ # containing metadata about the files belonging to that category. This is used
60
+ # to communicate what a Cookbook looks like when uploading to a Chef Server.
61
+ #
62
+ # example:
63
+ # {
64
+ # :recipes => [
65
+ # {
66
+ # name: "default.rb",
67
+ # path: "recipes/default.rb",
68
+ # checksum: "fb1f925dcd5fc4ebf682c4442a21c619",
69
+ # specificity: "default"
70
+ # }
71
+ # ]
72
+ # ...
73
+ # ...
74
+ # }
75
+ attr_reader :manifest
76
+
77
+ def_delegator :@metadata, :version
78
+
79
+ def initialize(name, path, metadata)
80
+ @cookbook_name = name
81
+ @path = Pathname.new(path)
82
+ @metadata = metadata
83
+ @files = Array.new
84
+ @manifest = Hashie::Mash.new(
85
+ recipes: Array.new,
86
+ definitions: Array.new,
87
+ libraries: Array.new,
88
+ attributes: Array.new,
89
+ files: Array.new,
90
+ templates: Array.new,
91
+ resources: Array.new,
92
+ providers: Array.new,
93
+ root_files: Array.new
94
+ )
95
+
96
+ load_files
97
+ end
98
+
99
+ # @return [Hash]
100
+ # an hash containing the checksums and expanded file paths of all of the
101
+ # files found in the instance of CachedCookbook
102
+ #
103
+ # example:
104
+ # {
105
+ # "da97c94bb6acb2b7900cbf951654fea3" => "/Users/reset/.ridley/nginx-0.101.2/README.md"
106
+ # }
107
+ def checksums
108
+ {}.tap do |checksums|
109
+ files.each do |file|
110
+ checksums[self.class.checksum(file)] = file
111
+ end
112
+ end
113
+ end
114
+
115
+ # @param [Symbol] category
116
+ # the category of file to generate metadata about
117
+ # @param [String] target
118
+ # the filepath to the file to get metadata information about
119
+ #
120
+ # @return [Hash]
121
+ # a Hash containing a name, path, checksum, and specificity key representing the
122
+ # metadata about a file contained in a Cookbook. This metadata is used when
123
+ # uploading a Cookbook's files to a Chef Server.
124
+ #
125
+ # @example
126
+ # file_metadata(:root_files, "somefile.h") => {
127
+ # name: "default.rb",
128
+ # path: "recipes/default.rb",
129
+ # checksum: "fb1f925dcd5fc4ebf682c4442a21c619",
130
+ # specificity: "default"
131
+ # }
132
+ def file_metadata(category, target)
133
+ target = Pathname.new(target)
134
+
135
+ {
136
+ name: target.basename.to_s,
137
+ path: target.relative_path_from(path).to_s,
138
+ checksum: self.class.checksum(target),
139
+ specificity: file_specificity(category, target)
140
+ }
141
+ end
142
+
143
+ # @param [Symbol] category
144
+ # @param [Pathname] target
145
+ #
146
+ # @return [String]
147
+ def file_specificity(category, target)
148
+ case category
149
+ when :files, :templates
150
+ relpath = target.relative_path_from(path).to_s
151
+ relpath.slice(/(.+)\/(.+)\/.+/, 2)
152
+ else
153
+ 'default'
154
+ end
155
+ end
156
+
157
+ # @return [String]
158
+ # the name of the cookbook and the version number separated by a dash (-).
159
+ #
160
+ # example:
161
+ # "nginx-0.101.2"
162
+ def name
163
+ "#{cookbook_name}-#{version}"
164
+ end
165
+
166
+ def validate
167
+ raise IOError, "No Cookbook found at: #{path}" unless path.exist?
168
+
169
+ unless quietly { syntax_checker.validate_ruby_files }
170
+ raise Ridley::Errors::CookbookSyntaxError, "Invalid ruby files in cookbook: #{name} (#{version})."
171
+ end
172
+ unless quietly { syntax_checker.validate_templates }
173
+ raise Ridley::Errors::CookbookSyntaxError, "Invalid template files in cookbook: #{name} (#{version})."
174
+ end
175
+
176
+ true
177
+ end
178
+
179
+ def to_hash
180
+ result = manifest.dup
181
+ result[:chef_type] = CHEF_TYPE
182
+ result[:name] = name
183
+ result[:cookbook_name] = cookbook_name
184
+ result[:version] = version
185
+ result[:metadata] = metadata
186
+ result.to_hash
187
+ end
188
+
189
+ def to_json(*args)
190
+ result = self.to_hash
191
+ result['json_class'] = CHEF_JSON_CLASS
192
+ result['frozen?'] = false
193
+ result.to_json(*args)
194
+ end
195
+
196
+ def to_s
197
+ "#{cookbook_name} (#{version}) '#{path}'"
198
+ end
199
+
200
+ def <=>(other)
201
+ [self.cookbook_name, self.version] <=> [other.cookbook_name, other.version]
202
+ end
203
+
204
+ private
205
+
206
+ attr_reader :files
207
+
208
+ def load_files
209
+ load_shallow(:recipes, 'recipes', '*.rb')
210
+ load_shallow(:definitions, 'definitions', '*.rb')
211
+ load_shallow(:libraries, 'libraries', '*.rb')
212
+ load_shallow(:attributes, 'attributes', '*.rb')
213
+ load_recursively(:files, "files", "*")
214
+ load_recursively(:templates, "templates", "*")
215
+ load_recursively(:resources, "resources", "*.rb")
216
+ load_recursively(:providers, "providers", "*.rb")
217
+ load_root
218
+ end
219
+
220
+ def load_root
221
+ [].tap do |files|
222
+ Dir.glob(path.join('*'), File::FNM_DOTMATCH).each do |file|
223
+ next if File.directory?(file)
224
+ @files << file
225
+ @manifest[:root_files] << file_metadata(:root_files, file)
226
+ end
227
+ end
228
+ end
229
+
230
+ def load_recursively(category, category_dir, glob)
231
+ [].tap do |files|
232
+ file_spec = path.join(category_dir, '**', glob)
233
+ Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file|
234
+ next if File.directory?(file)
235
+ @files << file
236
+ @manifest[category] << file_metadata(category, file)
237
+ end
238
+ end
239
+ end
240
+
241
+ def load_shallow(category, *path_glob)
242
+ [].tap do |files|
243
+ Dir[path.join(*path_glob)].each do |file|
244
+ @files << file
245
+ @manifest[category] << file_metadata(category, file)
246
+ end
247
+ end
248
+ end
249
+
250
+ def syntax_checker
251
+ @syntax_checker ||= Cookbook::SyntaxCheck.new(path.to_s)
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,552 @@
1
+ module Ridley::Chef
2
+ class Cookbook
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ #
5
+ # Borrowed and modified from: {https://raw.github.com/opscode/chef/11.4.0/lib/chef/cookbook/metadata.rb}
6
+ #
7
+ # Copyright:: Copyright 2008-2010 Opscode, Inc.
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+ #
21
+ # == Chef::Cookbook::Metadata
22
+ # Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
23
+ # about Chef Cookbooks.
24
+ class Metadata
25
+ class << self
26
+ def from_hash(hash)
27
+ new.from_hash(hash)
28
+ end
29
+ end
30
+
31
+ NAME = 'name'.freeze
32
+ DESCRIPTION = 'description'.freeze
33
+ LONG_DESCRIPTION = 'long_description'.freeze
34
+ MAINTAINER = 'maintainer'.freeze
35
+ MAINTAINER_EMAIL = 'maintainer_email'.freeze
36
+ LICENSE = 'license'.freeze
37
+ PLATFORMS = 'platforms'.freeze
38
+ DEPENDENCIES = 'dependencies'.freeze
39
+ RECOMMENDATIONS = 'recommendations'.freeze
40
+ SUGGESTIONS = 'suggestions'.freeze
41
+ CONFLICTING = 'conflicting'.freeze
42
+ PROVIDING = 'providing'.freeze
43
+ REPLACING = 'replacing'.freeze
44
+ ATTRIBUTES = 'attributes'.freeze
45
+ GROUPINGS = 'groupings'.freeze
46
+ RECIPES = 'recipes'.freeze
47
+ VERSION = 'version'.freeze
48
+
49
+ COMPARISON_FIELDS = [
50
+ :name, :description, :long_description, :maintainer,
51
+ :maintainer_email, :license, :platforms, :dependencies,
52
+ :recommendations, :suggestions, :conflicting, :providing,
53
+ :replacing, :attributes, :groupings, :recipes, :version
54
+ ]
55
+
56
+ include Chozo::Mixin::ParamsValidate
57
+ include Chozo::Mixin::FromFile
58
+
59
+ attr_reader :cookbook
60
+ attr_reader :platforms
61
+ attr_reader :dependencies
62
+ attr_reader :recommendations
63
+ attr_reader :suggestions
64
+ attr_reader :conflicting
65
+ attr_reader :providing
66
+ attr_reader :replacing
67
+ attr_reader :attributes
68
+ attr_reader :groupings
69
+ attr_reader :recipes
70
+ attr_reader :version
71
+
72
+ # Builds a new Chef::Cookbook::Metadata object.
73
+ #
74
+ # === Parameters
75
+ # cookbook<String>:: An optional cookbook object
76
+ # maintainer<String>:: An optional maintainer
77
+ # maintainer_email<String>:: An optional maintainer email
78
+ # license<String>::An optional license. Default is Apache v2.0
79
+ #
80
+ # === Returns
81
+ # metadata<Chef::Cookbook::Metadata>
82
+ def initialize(cookbook = nil, maintainer = 'YOUR_COMPANY_NAME', maintainer_email = 'YOUR_EMAIL', license = 'none')
83
+ @cookbook = cookbook
84
+ @name = cookbook ? cookbook.name : ""
85
+ @long_description = ""
86
+ self.maintainer(maintainer)
87
+ self.maintainer_email(maintainer_email)
88
+ self.license(license)
89
+ self.description('A fabulous new cookbook')
90
+ @platforms = Hashie::Mash.new
91
+ @dependencies = Hashie::Mash.new
92
+ @recommendations = Hashie::Mash.new
93
+ @suggestions = Hashie::Mash.new
94
+ @conflicting = Hashie::Mash.new
95
+ @providing = Hashie::Mash.new
96
+ @replacing = Hashie::Mash.new
97
+ @attributes = Hashie::Mash.new
98
+ @groupings = Hashie::Mash.new
99
+ @recipes = Hashie::Mash.new
100
+ @version = Solve::Version.new("0.0.0")
101
+ if cookbook
102
+ @recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e|
103
+ e = self.name if e =~ /::default$/
104
+ r[e] = ""
105
+ self.provides e
106
+ r
107
+ end
108
+ end
109
+ end
110
+
111
+ def ==(other)
112
+ COMPARISON_FIELDS.inject(true) do |equal_so_far, field|
113
+ equal_so_far && other.respond_to?(field) && (other.send(field) == send(field))
114
+ end
115
+ end
116
+
117
+ # Sets the cookbooks maintainer, or returns it.
118
+ #
119
+ # === Parameters
120
+ # maintainer<String>:: The maintainers name
121
+ #
122
+ # === Returns
123
+ # maintainer<String>:: Returns the current maintainer.
124
+ def maintainer(arg = nil)
125
+ set_or_return(
126
+ :maintainer,
127
+ arg,
128
+ :kind_of => [ String ]
129
+ )
130
+ end
131
+
132
+ # Sets the maintainers email address, or returns it.
133
+ #
134
+ # === Parameters
135
+ # maintainer_email<String>:: The maintainers email address
136
+ #
137
+ # === Returns
138
+ # maintainer_email<String>:: Returns the current maintainer email.
139
+ def maintainer_email(arg = nil)
140
+ set_or_return(
141
+ :maintainer_email,
142
+ arg,
143
+ :kind_of => [ String ]
144
+ )
145
+ end
146
+
147
+ # Sets the current license, or returns it.
148
+ #
149
+ # === Parameters
150
+ # license<String>:: The current license.
151
+ #
152
+ # === Returns
153
+ # license<String>:: Returns the current license
154
+ def license(arg = nil)
155
+ set_or_return(
156
+ :license,
157
+ arg,
158
+ :kind_of => [ String ]
159
+ )
160
+ end
161
+
162
+ # Sets the current description, or returns it. Should be short - one line only!
163
+ #
164
+ # === Parameters
165
+ # description<String>:: The new description
166
+ #
167
+ # === Returns
168
+ # description<String>:: Returns the description
169
+ def description(arg = nil)
170
+ set_or_return(
171
+ :description,
172
+ arg,
173
+ :kind_of => [ String ]
174
+ )
175
+ end
176
+
177
+ # Sets the current long description, or returns it. Might come from a README, say.
178
+ #
179
+ # === Parameters
180
+ # long_description<String>:: The new long description
181
+ #
182
+ # === Returns
183
+ # long_description<String>:: Returns the long description
184
+ def long_description(arg = nil)
185
+ set_or_return(
186
+ :long_description,
187
+ arg,
188
+ :kind_of => [ String ]
189
+ )
190
+ end
191
+
192
+ # Sets the current cookbook version, or returns it. Can be two or three digits, seperated
193
+ # by dots. ie: '2.1', '1.5.4' or '0.9'.
194
+ #
195
+ # === Parameters
196
+ # version<String>:: The curent version, as a string
197
+ #
198
+ # === Returns
199
+ # version<String>:: Returns the current version
200
+ def version(arg = nil)
201
+ if arg
202
+ @version = Solve::Version.new(arg)
203
+ end
204
+
205
+ @version.to_s
206
+ end
207
+
208
+ # Sets the name of the cookbook, or returns it.
209
+ #
210
+ # === Parameters
211
+ # name<String>:: The curent cookbook name.
212
+ #
213
+ # === Returns
214
+ # name<String>:: Returns the current cookbook name.
215
+ def name(arg = nil)
216
+ set_or_return(
217
+ :name,
218
+ arg,
219
+ :kind_of => [ String ]
220
+ )
221
+ end
222
+
223
+ # Adds a supported platform, with version checking strings.
224
+ #
225
+ # === Parameters
226
+ # platform<String>,<Symbol>:: The platform (like :ubuntu or :mac_os_x)
227
+ # version<String>:: A version constraint of the form "OP VERSION",
228
+ # where OP is one of < <= = > >= ~> and VERSION has
229
+ # the form x.y.z or x.y.
230
+ #
231
+ # === Returns
232
+ # versions<Array>:: Returns the list of versions for the platform
233
+ def supports(platform, *version_args)
234
+ version = version_args.first
235
+ @platforms[platform] = Solve::Constraint.new(version).to_s
236
+ @platforms[platform]
237
+ rescue Solve::Errors::InvalidConstraintFormat => ex
238
+ raise InvalidVersionConstraint, ex.to_s
239
+ end
240
+
241
+ # Adds a dependency on another cookbook, with version checking strings.
242
+ #
243
+ # === Parameters
244
+ # cookbook<String>:: The cookbook
245
+ # version<String>:: A version constraint of the form "OP VERSION",
246
+ # where OP is one of < <= = > >= ~> and VERSION has
247
+ # the form x.y.z or x.y.
248
+ #
249
+ # === Returns
250
+ # versions<Array>:: Returns the list of versions for the platform
251
+ def depends(cookbook, *version_args)
252
+ version = version_args.first
253
+ @dependencies[cookbook] = Solve::Constraint.new(version).to_s
254
+ @dependencies[cookbook]
255
+ rescue Solve::Errors::InvalidConstraintFormat => ex
256
+ raise InvalidVersionConstraint, ex.to_s
257
+ end
258
+
259
+ # Adds a recommendation for another cookbook, with version checking strings.
260
+ #
261
+ # === Parameters
262
+ # cookbook<String>:: The cookbook
263
+ # version<String>:: A version constraint of the form "OP VERSION",
264
+ # where OP is one of < <= = > >= ~> and VERSION has
265
+ # the form x.y.z or x.y.
266
+ #
267
+ # === Returns
268
+ # versions<Array>:: Returns the list of versions for the platform
269
+ def recommends(cookbook, *version_args)
270
+ version = version_args.first
271
+ @recommendations[cookbook] = Solve::Constraint.new(version).to_s
272
+ @recommendations[cookbook]
273
+ rescue Solve::Errors::InvalidConstraintFormat => ex
274
+ raise InvalidVersionConstraint, ex.to_s
275
+ end
276
+
277
+ # Adds a suggestion for another cookbook, with version checking strings.
278
+ #
279
+ # === Parameters
280
+ # cookbook<String>:: The cookbook
281
+ # version<String>:: A version constraint of the form "OP VERSION",
282
+ # where OP is one of < <= = > >= ~> and VERSION has the
283
+ # formx.y.z or x.y.
284
+ #
285
+ # === Returns
286
+ # versions<Array>:: Returns the list of versions for the platform
287
+ def suggests(cookbook, *version_args)
288
+ version = version_args.first
289
+ @suggestions[cookbook] = Solve::Constraint.new(version).to_s
290
+ @suggestions[cookbook]
291
+ rescue Solve::Errors::InvalidConstraintFormat => ex
292
+ raise InvalidVersionConstraint, ex.to_s
293
+ end
294
+
295
+ # Adds a conflict for another cookbook, with version checking strings.
296
+ #
297
+ # === Parameters
298
+ # cookbook<String>:: The cookbook
299
+ # version<String>:: A version constraint of the form "OP VERSION",
300
+ # where OP is one of < <= = > >= ~> and VERSION has
301
+ # the form x.y.z or x.y.
302
+ #
303
+ # === Returns
304
+ # versions<Array>:: Returns the list of versions for the platform
305
+ def conflicts(cookbook, *version_args)
306
+ version = version_args.first
307
+ @conflicting[cookbook] = Solve::Constraint.new(version).to_s
308
+ @conflicting[cookbook]
309
+ rescue Solve::Errors::InvalidConstraintFormat => ex
310
+ raise InvalidVersionConstraint, ex.to_s
311
+ end
312
+
313
+ # Adds a recipe, definition, or resource provided by this cookbook.
314
+ #
315
+ # Recipes are specified as normal
316
+ # Definitions are followed by (), and can include :params for prototyping
317
+ # Resources are the stringified version (service[apache2])
318
+ #
319
+ # === Parameters
320
+ # recipe, definition, resource<String>:: The thing we provide
321
+ # version<String>:: A version constraint of the form "OP VERSION",
322
+ # where OP is one of < <= = > >= ~> and VERSION has
323
+ # the form x.y.z or x.y.
324
+ #
325
+ # === Returns
326
+ # versions<Array>:: Returns the list of versions for the platform
327
+ def provides(cookbook, *version_args)
328
+ version = version_args.first
329
+ @providing[cookbook] = Solve::Constraint.new(version).to_s
330
+ @providing[cookbook]
331
+ rescue Solve::Errors::InvalidConstraintFormat => ex
332
+ raise InvalidVersionConstraint, ex.to_s
333
+ end
334
+
335
+ # Adds a cookbook that is replaced by this one, with version checking strings.
336
+ #
337
+ # === Parameters
338
+ # cookbook<String>:: The cookbook we replace
339
+ # version<String>:: A version constraint of the form "OP VERSION",
340
+ # where OP is one of < <= = > >= ~> and VERSION has the form x.y.z or x.y.
341
+ #
342
+ # === Returns
343
+ # versions<Array>:: Returns the list of versions for the platform
344
+ def replaces(cookbook, *version_args)
345
+ version = version_args.first
346
+ @replacing[cookbook] = Solve::Constraint.new(version).to_s
347
+ @replacing[cookbook]
348
+ rescue Solve::Errors::InvalidConstraintFormat => ex
349
+ raise InvalidVersionConstraint, ex.to_s
350
+ end
351
+
352
+ # Adds a description for a recipe.
353
+ #
354
+ # === Parameters
355
+ # recipe<String>:: The recipe
356
+ # description<String>:: The description of the recipe
357
+ #
358
+ # === Returns
359
+ # description<String>:: Returns the current description
360
+ def recipe(name, description)
361
+ @recipes[name] = description
362
+ end
363
+
364
+ # Adds an attribute )hat a user needs to configure for this cookbook. Takes
365
+ # a name (with the / notation for a nested attribute), followed by any of
366
+ # these options
367
+ #
368
+ # display_name<String>:: What a UI should show for this attribute
369
+ # description<String>:: A hint as to what this attr is for
370
+ # choice<Array>:: An array of choices to present to the user.
371
+ # calculated<Boolean>:: If true, the default value is calculated by the recipe and cannot be displayed.
372
+ # type<String>:: "string" or "array" - default is "string" ("hash" is supported for backwards compatibility)
373
+ # required<String>:: Whether this attr is 'required', 'recommended' or 'optional' - default 'optional' (true/false values also supported for backwards compatibility)
374
+ # recipes<Array>:: An array of recipes which need this attr set.
375
+ # default<String>,<Array>,<Hash>:: The default value
376
+ #
377
+ # === Parameters
378
+ # name<String>:: The name of the attribute ('foo', or 'apache2/log_dir')
379
+ # options<Hash>:: The description of the options
380
+ #
381
+ # === Returns
382
+ # options<Hash>:: Returns the current options hash
383
+ def attribute(name, options)
384
+ validate(
385
+ options,
386
+ {
387
+ :display_name => { :kind_of => String },
388
+ :description => { :kind_of => String },
389
+ :choice => { :kind_of => [ Array ], :default => [] },
390
+ :calculated => { :equal_to => [ true, false ], :default => false },
391
+ :type => { :equal_to => [ "string", "array", "hash", "symbol" ], :default => "string" },
392
+ :required => { :equal_to => [ "required", "recommended", "optional", true, false ], :default => "optional" },
393
+ :recipes => { :kind_of => [ Array ], :default => [] },
394
+ :default => { :kind_of => [ String, Array, Hash ] }
395
+ }
396
+ )
397
+ options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil?
398
+ validate_string_array(options[:choice])
399
+ validate_calculated_default_rule(options)
400
+ validate_choice_default_rule(options)
401
+
402
+ @attributes[name] = options
403
+ @attributes[name]
404
+ end
405
+
406
+ def grouping(name, options)
407
+ validate(
408
+ options,
409
+ {
410
+ :title => { :kind_of => String },
411
+ :description => { :kind_of => String }
412
+ }
413
+ )
414
+ @groupings[name] = options
415
+ @groupings[name]
416
+ end
417
+
418
+ def to_hash
419
+ {
420
+ NAME => self.name,
421
+ DESCRIPTION => self.description,
422
+ LONG_DESCRIPTION => self.long_description,
423
+ MAINTAINER => self.maintainer,
424
+ MAINTAINER_EMAIL => self.maintainer_email,
425
+ LICENSE => self.license,
426
+ PLATFORMS => self.platforms,
427
+ DEPENDENCIES => self.dependencies,
428
+ RECOMMENDATIONS => self.recommendations,
429
+ SUGGESTIONS => self.suggestions,
430
+ CONFLICTING => self.conflicting,
431
+ PROVIDING => self.providing,
432
+ REPLACING => self.replacing,
433
+ ATTRIBUTES => self.attributes,
434
+ GROUPINGS => self.groupings,
435
+ RECIPES => self.recipes,
436
+ VERSION => self.version
437
+ }
438
+ end
439
+
440
+ def from_hash(o)
441
+ @name = o[NAME] if o.has_key?(NAME)
442
+ @description = o[DESCRIPTION] if o.has_key?(DESCRIPTION)
443
+ @long_description = o[LONG_DESCRIPTION] if o.has_key?(LONG_DESCRIPTION)
444
+ @maintainer = o[MAINTAINER] if o.has_key?(MAINTAINER)
445
+ @maintainer_email = o[MAINTAINER_EMAIL] if o.has_key?(MAINTAINER_EMAIL)
446
+ @license = o[LICENSE] if o.has_key?(LICENSE)
447
+ @platforms = o[PLATFORMS] if o.has_key?(PLATFORMS)
448
+ @dependencies = handle_deprecated_constraints(o[DEPENDENCIES]) if o.has_key?(DEPENDENCIES)
449
+ @recommendations = handle_deprecated_constraints(o[RECOMMENDATIONS]) if o.has_key?(RECOMMENDATIONS)
450
+ @suggestions = handle_deprecated_constraints(o[SUGGESTIONS]) if o.has_key?(SUGGESTIONS)
451
+ @conflicting = handle_deprecated_constraints(o[CONFLICTING]) if o.has_key?(CONFLICTING)
452
+ @providing = o[PROVIDING] if o.has_key?(PROVIDING)
453
+ @replacing = handle_deprecated_constraints(o[REPLACING]) if o.has_key?(REPLACING)
454
+ @attributes = o[ATTRIBUTES] if o.has_key?(ATTRIBUTES)
455
+ @groupings = o[GROUPINGS] if o.has_key?(GROUPINGS)
456
+ @recipes = o[RECIPES] if o.has_key?(RECIPES)
457
+ @version = o[VERSION] if o.has_key?(VERSION)
458
+ self
459
+ end
460
+
461
+ private
462
+
463
+ # Verify that the given array is an array of strings
464
+ #
465
+ # Raise an exception if the members of the array are not Strings
466
+ #
467
+ # === Parameters
468
+ # arry<Array>:: An array to be validated
469
+ def validate_string_array(arry)
470
+ if arry.kind_of?(Array)
471
+ arry.each do |choice|
472
+ validate( {:choice => choice}, {:choice => {:kind_of => String}} )
473
+ end
474
+ end
475
+ end
476
+
477
+ # For backwards compatibility, remap Boolean values to String
478
+ # true is mapped to "required"
479
+ # false is mapped to "optional"
480
+ #
481
+ # === Parameters
482
+ # required_attr<String><Boolean>:: The value of options[:required]
483
+ #
484
+ # === Returns
485
+ # required_attr<String>:: "required", "recommended", or "optional"
486
+ def remap_required_attribute(value)
487
+ case value
488
+ when true
489
+ value = "required"
490
+ when false
491
+ value = "optional"
492
+ end
493
+ value
494
+ end
495
+
496
+ def validate_calculated_default_rule(options)
497
+ calculated_conflict = ((options[:default].is_a?(Array) && !options[:default].empty?) ||
498
+ (options[:default].is_a?(String) && !options[:default] != "")) &&
499
+ options[:calculated] == true
500
+ raise ArgumentError, "Default cannot be specified if calculated is true!" if calculated_conflict
501
+ end
502
+
503
+ def validate_choice_default_rule(options)
504
+ return if !options[:choice].is_a?(Array) || options[:choice].empty?
505
+
506
+ if options[:default].is_a?(String) && options[:default] != ""
507
+ raise ArgumentError, "Default must be one of your choice values!" if options[:choice].index(options[:default]) == nil
508
+ end
509
+
510
+ if options[:default].is_a?(Array) && !options[:default].empty?
511
+ options[:default].each do |val|
512
+ raise ArgumentError, "Default values must be a subset of your choice values!" if options[:choice].index(val) == nil
513
+ end
514
+ end
515
+ end
516
+
517
+ # This method translates version constraint strings from
518
+ # cookbooks with the old format.
519
+ #
520
+ # Before we began respecting version constraints, we allowed
521
+ # multiple constraints to be placed on cookbooks, as well as the
522
+ # << and >> operators, which are now just < and >. For
523
+ # specifications with more than one constraint, we return an
524
+ # empty array (otherwise, we're silently abiding only part of
525
+ # the contract they have specified to us). If there is only one
526
+ # constraint, we are replacing the old << and >> with the new <
527
+ # and >.
528
+ def handle_deprecated_constraints(specification)
529
+ specification.inject(Hashie::Mash.new) do |acc, (cb, constraints)|
530
+ constraints = Array(constraints)
531
+ acc[cb] = (constraints.empty? || constraints.size > 1) ? [] : constraints.first.gsub(/>>/, '>').gsub(/<</, '<')
532
+ acc
533
+ end
534
+ end
535
+ end
536
+
537
+ #== Chef::Cookbook::MinimalMetadata
538
+ # MinimalMetadata is a duck type of Cookbook::Metadata, used
539
+ # internally by Chef Server when determining the optimal set of
540
+ # cookbooks for a node.
541
+ #
542
+ # MinimalMetadata objects typically contain only enough information
543
+ # to solve the cookbook collection for a run list, but not enough to
544
+ # generate the proper response
545
+ class MinimalMetadata < Metadata
546
+ def initialize(name, params)
547
+ @name = name
548
+ from_hash(params)
549
+ end
550
+ end
551
+ end
552
+ end