ridley 0.7.0.rc4 → 0.7.0

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/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