berkshelf 1.1.6 → 1.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/.cane +4 -0
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +25 -0
  4. data/CONTRIBUTING.md +9 -0
  5. data/Gemfile +1 -7
  6. data/Guardfile +4 -2
  7. data/LICENSE +4 -4
  8. data/README.md +34 -11
  9. data/Thorfile +12 -11
  10. data/berkshelf-complete.sh +68 -0
  11. data/berkshelf.gemspec +23 -10
  12. data/bin/berks +2 -0
  13. data/features/config.feature +5 -4
  14. data/features/configure_command.feature +1 -0
  15. data/features/install_command.feature +7 -43
  16. data/features/step_definitions/configure_cli_steps.rb +6 -0
  17. data/features/step_definitions/filesystem_steps.rb +3 -1
  18. data/features/step_definitions/json_steps.rb +7 -0
  19. data/features/support/env.rb +3 -8
  20. data/features/upload_command.feature +49 -101
  21. data/generator_files/Vagrantfile.erb +1 -1
  22. data/generator_files/gitignore.erb +6 -1
  23. data/lib/berkshelf.rb +17 -26
  24. data/lib/berkshelf/base_generator.rb +8 -7
  25. data/lib/berkshelf/berksfile.rb +33 -13
  26. data/lib/berkshelf/cached_cookbook.rb +16 -37
  27. data/lib/berkshelf/chef.rb +11 -0
  28. data/lib/berkshelf/chef/config.rb +93 -0
  29. data/lib/berkshelf/chef/cookbook.rb +8 -0
  30. data/lib/berkshelf/chef/cookbook/chefignore.rb +60 -0
  31. data/lib/berkshelf/chef/cookbook/metadata.rb +556 -0
  32. data/lib/berkshelf/chef/cookbook/syntax_check.rb +158 -0
  33. data/lib/berkshelf/chef/digester.rb +67 -0
  34. data/lib/berkshelf/cli.rb +18 -4
  35. data/lib/berkshelf/command.rb +117 -0
  36. data/lib/berkshelf/community_rest.rb +135 -0
  37. data/lib/berkshelf/config.rb +16 -50
  38. data/lib/berkshelf/cookbook_generator.rb +5 -9
  39. data/lib/berkshelf/cookbook_source.rb +1 -1
  40. data/lib/berkshelf/cookbook_store.rb +5 -1
  41. data/lib/berkshelf/downloader.rb +4 -4
  42. data/lib/berkshelf/errors.rb +6 -0
  43. data/lib/berkshelf/formatters.rb +3 -3
  44. data/lib/berkshelf/formatters/human_readable.rb +1 -1
  45. data/lib/berkshelf/formatters/json.rb +1 -1
  46. data/lib/berkshelf/git.rb +2 -2
  47. data/lib/berkshelf/init_generator.rb +5 -11
  48. data/lib/berkshelf/location.rb +7 -6
  49. data/lib/berkshelf/locations/chef_api_location.rb +54 -108
  50. data/lib/berkshelf/locations/git_location.rb +1 -1
  51. data/lib/berkshelf/locations/github_location.rb +1 -1
  52. data/lib/berkshelf/locations/path_location.rb +22 -3
  53. data/lib/berkshelf/locations/site_location.rb +31 -99
  54. data/lib/berkshelf/mixin.rb +10 -0
  55. data/lib/berkshelf/mixin/checksum.rb +16 -0
  56. data/lib/berkshelf/mixin/params_validate.rb +218 -0
  57. data/lib/berkshelf/mixin/path_helpers.rb +30 -0
  58. data/lib/berkshelf/mixin/shell_out.rb +23 -0
  59. data/lib/berkshelf/resolver.rb +5 -3
  60. data/lib/berkshelf/ui.rb +5 -1
  61. data/lib/berkshelf/uploader.rb +11 -5
  62. data/lib/berkshelf/vagrant.rb +1 -1
  63. data/lib/berkshelf/vagrant/action/clean.rb +1 -1
  64. data/lib/berkshelf/vagrant/action/install.rb +1 -1
  65. data/lib/berkshelf/vagrant/action/set_ui.rb +2 -2
  66. data/lib/berkshelf/vagrant/action/upload.rb +1 -1
  67. data/lib/berkshelf/vagrant/action/validate.rb +1 -1
  68. data/lib/berkshelf/vagrant/config.rb +1 -1
  69. data/lib/berkshelf/vagrant/middleware.rb +1 -1
  70. data/lib/berkshelf/version.rb +1 -1
  71. data/lib/thor/monkies/shell.rb +8 -0
  72. data/spec/fixtures/cookbooks/example_cookbook-0.5.0/metadata.rb +1 -1
  73. data/spec/fixtures/cookbooks/example_cookbook/metadata.rb +1 -1
  74. data/spec/fixtures/cookbooks/nginx-0.100.5/templates/default/plugins/nginx.rb.erb +1 -1
  75. data/spec/spec_helper.rb +81 -21
  76. data/spec/support/chef_api.rb +29 -28
  77. data/spec/support/knife.rb +2 -2
  78. data/spec/support/test_generators.rb +27 -0
  79. data/spec/unit/berkshelf/berksfile_spec.rb +2 -2
  80. data/spec/unit/berkshelf/cached_cookbook_spec.rb +10 -2
  81. data/spec/unit/berkshelf/community_rest_spec.rb +120 -0
  82. data/spec/unit/berkshelf/config_spec.rb +0 -12
  83. data/spec/unit/berkshelf/cookbook_generator_spec.rb +65 -67
  84. data/spec/unit/berkshelf/cookbook_source_spec.rb +1 -1
  85. data/spec/unit/berkshelf/git_spec.rb +17 -12
  86. data/spec/unit/berkshelf/init_generator_spec.rb +126 -120
  87. data/spec/unit/berkshelf/location_spec.rb +2 -2
  88. data/spec/unit/berkshelf/locations/chef_api_location_spec.rb +90 -186
  89. data/spec/unit/berkshelf/locations/git_location_spec.rb +87 -82
  90. data/spec/unit/berkshelf/locations/path_location_spec.rb +34 -35
  91. data/spec/unit/berkshelf/locations/site_location_spec.rb +12 -115
  92. data/spec/unit/berkshelf/resolver_spec.rb +96 -87
  93. data/spec/unit/berkshelf/ui_spec.rb +132 -0
  94. data/spec/unit/berkshelf/uploader_spec.rb +18 -12
  95. data/spec/unit/chef/config_spec.rb +9 -0
  96. data/spec/unit/chef/cookbook/metadata_spec.rb +5 -0
  97. data/spec/unit/chef/digester_spec.rb +41 -0
  98. metadata +263 -28
@@ -1,15 +1,9 @@
1
- if Berkshelf.chef_11?
2
- require 'chef/digester'
3
- else
4
- require 'chef/checksum_cache'
5
- end
6
-
7
- require 'chef/cookbook/syntax_check'
8
-
9
1
  module Berkshelf
10
- # @author Jamie Winsor <jamie@vialstudios.com>
2
+ # @author Jamie Winsor <reset@riotgames.com>
11
3
  class CachedCookbook
12
4
  class << self
5
+ include Berkshelf::Mixin::Checksum
6
+
13
7
  # Creates a new instance of Berkshelf::CachedCookbook from a path on disk that
14
8
  # contains a Cookbook. The name of the Cookbook will be determined first by the
15
9
  # name attribute of the metadata.rb file if it is present. If the name attribute
@@ -21,16 +15,11 @@ module Berkshelf
21
15
  #
22
16
  # @return [Berkshelf::CachedCookbook]
23
17
  def from_path(path)
24
- path = Pathname.new(path)
25
- metadata = Chef::Cookbook::Metadata.new
26
-
27
- begin
28
- metadata.from_file(path.join("metadata.rb").to_s)
29
- rescue IOError
30
- raise CookbookNotFound, "No 'metadata.rb' file found at: '#{path}'"
31
- end
18
+ path = Pathname.new(path)
19
+ metadata = Berkshelf::Chef::Cookbook::Metadata.from_file(path.join('metadata.rb'))
32
20
 
33
21
  name = metadata.name.empty? ? File.basename(path) : metadata.name
22
+ metadata.name(name) if metadata.name.empty?
34
23
 
35
24
  new(name, path, metadata)
36
25
  end
@@ -42,19 +31,12 @@ module Berkshelf
42
31
  # an instance of CachedCookbook initialized by the contents found at the
43
32
  # given path.
44
33
  def from_store_path(path)
45
- path = Pathname.new(path)
34
+ path = Pathname.new(path)
46
35
  cached_name = File.basename(path.to_s).slice(DIRNAME_REGEXP, 1)
47
36
  return nil if cached_name.nil?
48
37
 
49
- metadata = Chef::Cookbook::Metadata.new
50
-
51
- begin
52
- metadata.from_file(path.join("metadata.rb").to_s)
53
- rescue IOError
54
- raise CookbookNotFound, "No 'metadata.rb' file found at: '#{path}'"
55
- end
56
-
57
- metadata.name cached_name if metadata.name.empty?
38
+ metadata = Berkshelf::Chef::Cookbook::Metadata.from_file(path.join('metadata.rb'))
39
+ metadata.name(cached_name) if metadata.name.empty?
58
40
 
59
41
  new(cached_name, path, metadata)
60
42
  end
@@ -66,11 +48,7 @@ module Berkshelf
66
48
  # a checksum that can be used to uniquely identify the file understood
67
49
  # by a Chef Server.
68
50
  def checksum(filepath)
69
- if Berkshelf.chef_11?
70
- Chef::Digester.generate_md5_checksum_for_file(filepath)
71
- else
72
- Chef::ChecksumCache.generate_md5_checksum_for_file(filepath)
73
- end
51
+ Berkshelf::Chef::Digester.md5_checksum_for_file(filepath)
74
52
  end
75
53
  end
76
54
 
@@ -84,8 +62,8 @@ module Berkshelf
84
62
  attr_reader :path
85
63
  attr_reader :metadata
86
64
 
87
- # @return [Mash]
88
- # a Mash containing Cookbook file category names as keys and an Array of Hashes
65
+ # @return [Hashie::Mash]
66
+ # a Hashie::Mash containing Cookbook file category names as keys and an Array of Hashes
89
67
  # containing metadata about the files belonging to that category. This is used
90
68
  # to communicate what a Cookbook looks like when uploading to a Chef Server.
91
69
  #
@@ -111,7 +89,7 @@ module Berkshelf
111
89
  @path = Pathname.new(path)
112
90
  @metadata = metadata
113
91
  @files = Array.new
114
- @manifest = Mash.new(
92
+ @manifest = Hashie::Mash.new(
115
93
  recipes: Array.new,
116
94
  definitions: Array.new,
117
95
  libraries: Array.new,
@@ -210,7 +188,8 @@ module Berkshelf
210
188
  result['cookbook_name'] = cookbook_name
211
189
  result['version'] = version
212
190
  result['metadata'] = metadata
213
- result.to_hash
191
+ result['chef_type']
192
+ result
214
193
  end
215
194
 
216
195
  def to_json(*a)
@@ -241,7 +220,7 @@ module Berkshelf
241
220
  end
242
221
 
243
222
  def syntax_checker
244
- @syntax_checker ||= Chef::Cookbook::SyntaxCheck.new(path.to_s)
223
+ @syntax_checker ||= Berkshelf::Chef::Cookbook::SyntaxCheck.new(path.to_s)
245
224
  end
246
225
 
247
226
  def load_files
@@ -0,0 +1,11 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <reset@riotgames.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 :Config, 'berkshelf/chef/config'
8
+ autoload :Cookbook, 'berkshelf/chef/cookbook'
9
+ autoload :Digester, 'berkshelf/chef/digester'
10
+ end
11
+ end
@@ -0,0 +1,93 @@
1
+ require 'socket'
2
+ require 'berkshelf/mixin'
3
+ require 'mixlib/config'
4
+
5
+ module Berkshelf::Chef
6
+ # @author Jamie Winsor <reset@riotgames.com>
7
+ #
8
+ # Inspired by and a dependency-free replacement for {https://raw.github.com/opscode/chef/11.4.0/lib/chef/config.rb}
9
+ class Config
10
+ # List taken from: http://wiki.opscode.com/display/chef/Chef+Configuration+Settings
11
+ # Listed in order of preferred preference
12
+ KNIFE_LOCATIONS = [
13
+ './.chef/knife.rb',
14
+ '~/.chef/knife.rb',
15
+ '/etc/chef/solo.rb',
16
+ '/etc/chef/client.rb'
17
+ ].freeze
18
+
19
+ class << self
20
+ # Load and return a Chef::Config for Berkshelf. The location of the configuration to be loaded
21
+ # can be configured by setting a value for {Berkshelf::Chef::Config.path=}
22
+ #
23
+ # @return [Berkshelf::Chef::Config]
24
+ def instance
25
+ @instance ||= begin
26
+ self.from_file(File.expand_path(path))
27
+ self
28
+ rescue
29
+ self
30
+ end
31
+ end
32
+
33
+ # Return the most sensible path to the Chef configuration file. This can be configured by setting a
34
+ # value for the 'BERKSHELF_CHEF_CONFIG' environment variable.
35
+ #
36
+ # If no value is set for the environment variable then a search will begin for a configuration at these paths
37
+ # in this order:
38
+ #
39
+ # * './chef/knife.rb'
40
+ # * '~/.chef/knife.rb',
41
+ # * '/etc/chef/solo.rb'
42
+ # * '/etc/chef/client.rb'
43
+ #
44
+ # @return [String, nil]
45
+ def path
46
+ @path ||= begin
47
+ possibles = KNIFE_LOCATIONS.dup
48
+
49
+ unless ENV['BERKSHELF_CHEF_CONFIG'].nil?
50
+ possibles.unshift(ENV['BERKSHELF_CHEF_CONFIG'])
51
+ end
52
+
53
+ location = possibles.find do |location|
54
+ File.exists?(File.expand_path(location))
55
+ end
56
+ location ||= "~/.chef/knife.rb"
57
+
58
+ File.expand_path(location)
59
+ end
60
+ end
61
+
62
+ # Set the path the Chef configuration can be found in
63
+ #
64
+ # @param [String] value
65
+ def path=(value)
66
+ @instance = nil
67
+ @path = value
68
+ end
69
+ end
70
+
71
+ extend Berkshelf::Mixin::PathHelpers
72
+ extend Mixlib::Config
73
+
74
+ node_name Socket.gethostname
75
+ chef_server_url "http://localhost:4000"
76
+ client_key platform_specific_path("/etc/chef/client.pem")
77
+ validation_key platform_specific_path("/etc/chef/validation.pem")
78
+ validation_client_name "chef-validator"
79
+
80
+ cookbook_copyright "YOUR_NAME"
81
+ cookbook_email "YOUR_EMAIL"
82
+ cookbook_license "reserved"
83
+
84
+ # history: prior to Chef 11, the cache implementation was based on
85
+ # moneta and configured via cache_options[:path]. Knife configs
86
+ # generated with Chef 11 will have `syntax_check_cache_path`, but older
87
+ # configs will have `cache_options[:path]`. `cache_options` is marked
88
+ # deprecated in chef/config.rb but doesn't currently trigger a warning.
89
+ # See also: CHEF-3715
90
+ syntax_check_cache_path Dir.mktmpdir
91
+ cache_options path: syntax_check_cache_path
92
+ end
93
+ end
@@ -0,0 +1,8 @@
1
+ module Berkshelf::Chef
2
+ # @author Jamie Winsor <reset@riotgames.com>
3
+ module Cookbook
4
+ autoload :Chefignore, 'berkshelf/chef/cookbook/chefignore'
5
+ autoload :Metadata, 'berkshelf/chef/cookbook/metadata'
6
+ autoload :SyntaxCheck, 'berkshelf/chef/cookbook/syntax_check'
7
+ end
8
+ end
@@ -0,0 +1,60 @@
1
+ module Berkshelf::Chef::Cookbook
2
+ # Borrowed and modified from: {https://raw.github.com/opscode/chef/11.4.0/lib/chef/cookbook/chefignore.rb}
3
+ #
4
+ # Copyright:: Copyright (c) 2011 Opscode, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ class Chefignore
18
+ FILENAME = "chefignore".freeze
19
+ COMMENTS_AND_WHITESPACE = /^\s*(?:#.*)?$/
20
+
21
+ attr_reader :ignores
22
+
23
+ def initialize(ignore_file_or_repo)
24
+ @ignore_file = find_ignore_file(ignore_file_or_repo)
25
+ @ignores = parse_ignore_file
26
+ end
27
+
28
+ def remove_ignores_from(file_list)
29
+ Array(file_list).inject([]) do |unignored, file|
30
+ ignored?(file) ? unignored : unignored << file
31
+ end
32
+ end
33
+
34
+ def ignored?(file_name)
35
+ @ignores.any? {|glob| File.fnmatch?(glob, file_name)}
36
+ end
37
+
38
+ private
39
+
40
+ def parse_ignore_file
41
+ ignore_globs = []
42
+ if File.exist?(@ignore_file) && File.readable?(@ignore_file)
43
+ File.foreach(@ignore_file) do |line|
44
+ ignore_globs << line.strip unless line =~ COMMENTS_AND_WHITESPACE
45
+ end
46
+ else
47
+ # Log a warning when Berkshelf gets a logger
48
+ end
49
+ ignore_globs
50
+ end
51
+
52
+ def find_ignore_file(path)
53
+ if File.basename(path) =~ /#{FILENAME}/
54
+ path
55
+ else
56
+ File.join(path, FILENAME)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,556 @@
1
+ module Berkshelf::Chef::Cookbook
2
+ # @author Jamie Winsor <reset@riotgames.com>
3
+ #
4
+ # Borrowed and modified from: {https://raw.github.com/opscode/chef/11.4.0/lib/chef/cookbook/metadata.rb}
5
+ #
6
+ # Copyright:: Copyright 2008-2010 Opscode, Inc.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+ # == Chef::Cookbook::Metadata
21
+ # Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
22
+ # about Chef Cookbooks.
23
+ class Metadata
24
+ class << self
25
+ def from_hash(hash)
26
+ new.from_hash(hash)
27
+ end
28
+ end
29
+
30
+ NAME = 'name'.freeze
31
+ DESCRIPTION = 'description'.freeze
32
+ LONG_DESCRIPTION = 'long_description'.freeze
33
+ MAINTAINER = 'maintainer'.freeze
34
+ MAINTAINER_EMAIL = 'maintainer_email'.freeze
35
+ LICENSE = 'license'.freeze
36
+ PLATFORMS = 'platforms'.freeze
37
+ DEPENDENCIES = 'dependencies'.freeze
38
+ RECOMMENDATIONS = 'recommendations'.freeze
39
+ SUGGESTIONS = 'suggestions'.freeze
40
+ CONFLICTING = 'conflicting'.freeze
41
+ PROVIDING = 'providing'.freeze
42
+ REPLACING = 'replacing'.freeze
43
+ ATTRIBUTES = 'attributes'.freeze
44
+ GROUPINGS = 'groupings'.freeze
45
+ RECIPES = 'recipes'.freeze
46
+ VERSION = 'version'.freeze
47
+
48
+ COMPARISON_FIELDS = [
49
+ :name, :description, :long_description, :maintainer,
50
+ :maintainer_email, :license, :platforms, :dependencies,
51
+ :recommendations, :suggestions, :conflicting, :providing,
52
+ :replacing, :attributes, :groupings, :recipes, :version
53
+ ]
54
+
55
+ include Berkshelf::Mixin::ParamsValidate
56
+ include Chozo::Mixin::FromFile
57
+
58
+ attr_reader :cookbook
59
+ attr_reader :platforms
60
+ attr_reader :dependencies
61
+ attr_reader :recommendations
62
+ attr_reader :suggestions
63
+ attr_reader :conflicting
64
+ attr_reader :providing
65
+ attr_reader :replacing
66
+ attr_reader :attributes
67
+ attr_reader :groupings
68
+ attr_reader :recipes
69
+ attr_reader :version
70
+
71
+ # Builds a new Chef::Cookbook::Metadata object.
72
+ #
73
+ # === Parameters
74
+ # cookbook<String>:: An optional cookbook object
75
+ # maintainer<String>:: An optional maintainer
76
+ # maintainer_email<String>:: An optional maintainer email
77
+ # license<String>::An optional license. Default is Apache v2.0
78
+ #
79
+ # === Returns
80
+ # metadata<Chef::Cookbook::Metadata>
81
+ def initialize(cookbook = nil, maintainer = 'YOUR_COMPANY_NAME', maintainer_email = 'YOUR_EMAIL', license = 'none')
82
+ @cookbook = cookbook
83
+ @name = cookbook ? cookbook.name : ""
84
+ @long_description = ""
85
+ self.maintainer(maintainer)
86
+ self.maintainer_email(maintainer_email)
87
+ self.license(license)
88
+ self.description('A fabulous new cookbook')
89
+ @platforms = Hashie::Mash.new
90
+ @dependencies = Hashie::Mash.new
91
+ @recommendations = Hashie::Mash.new
92
+ @suggestions = Hashie::Mash.new
93
+ @conflicting = Hashie::Mash.new
94
+ @providing = Hashie::Mash.new
95
+ @replacing = Hashie::Mash.new
96
+ @attributes = Hashie::Mash.new
97
+ @groupings = Hashie::Mash.new
98
+ @recipes = Hashie::Mash.new
99
+ @version = Solve::Version.new("0.0.0")
100
+ if cookbook
101
+ @recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e|
102
+ e = self.name if e =~ /::default$/
103
+ r[e] = ""
104
+ self.provides e
105
+ r
106
+ end
107
+ end
108
+ end
109
+
110
+ def ==(other)
111
+ COMPARISON_FIELDS.inject(true) do |equal_so_far, field|
112
+ equal_so_far && other.respond_to?(field) && (other.send(field) == send(field))
113
+ end
114
+ end
115
+
116
+ # Sets the cookbooks maintainer, or returns it.
117
+ #
118
+ # === Parameters
119
+ # maintainer<String>:: The maintainers name
120
+ #
121
+ # === Returns
122
+ # maintainer<String>:: Returns the current maintainer.
123
+ def maintainer(arg = nil)
124
+ set_or_return(
125
+ :maintainer,
126
+ arg,
127
+ :kind_of => [ String ]
128
+ )
129
+ end
130
+
131
+ # Sets the maintainers email address, or returns it.
132
+ #
133
+ # === Parameters
134
+ # maintainer_email<String>:: The maintainers email address
135
+ #
136
+ # === Returns
137
+ # maintainer_email<String>:: Returns the current maintainer email.
138
+ def maintainer_email(arg = nil)
139
+ set_or_return(
140
+ :maintainer_email,
141
+ arg,
142
+ :kind_of => [ String ]
143
+ )
144
+ end
145
+
146
+ # Sets the current license, or returns it.
147
+ #
148
+ # === Parameters
149
+ # license<String>:: The current license.
150
+ #
151
+ # === Returns
152
+ # license<String>:: Returns the current license
153
+ def license(arg = nil)
154
+ set_or_return(
155
+ :license,
156
+ arg,
157
+ :kind_of => [ String ]
158
+ )
159
+ end
160
+
161
+ # Sets the current description, or returns it. Should be short - one line only!
162
+ #
163
+ # === Parameters
164
+ # description<String>:: The new description
165
+ #
166
+ # === Returns
167
+ # description<String>:: Returns the description
168
+ def description(arg = nil)
169
+ set_or_return(
170
+ :description,
171
+ arg,
172
+ :kind_of => [ String ]
173
+ )
174
+ end
175
+
176
+ # Sets the current long description, or returns it. Might come from a README, say.
177
+ #
178
+ # === Parameters
179
+ # long_description<String>:: The new long description
180
+ #
181
+ # === Returns
182
+ # long_description<String>:: Returns the long description
183
+ def long_description(arg = nil)
184
+ set_or_return(
185
+ :long_description,
186
+ arg,
187
+ :kind_of => [ String ]
188
+ )
189
+ end
190
+
191
+ # Sets the current cookbook version, or returns it. Can be two or three digits, seperated
192
+ # by dots. ie: '2.1', '1.5.4' or '0.9'.
193
+ #
194
+ # === Parameters
195
+ # version<String>:: The curent version, as a string
196
+ #
197
+ # === Returns
198
+ # version<String>:: Returns the current version
199
+ def version(arg = nil)
200
+ if arg
201
+ @version = Solve::Version.new(arg)
202
+ end
203
+
204
+ @version.to_s
205
+ end
206
+
207
+ # Sets the name of the cookbook, or returns it.
208
+ #
209
+ # === Parameters
210
+ # name<String>:: The curent cookbook name.
211
+ #
212
+ # === Returns
213
+ # name<String>:: Returns the current cookbook name.
214
+ def name(arg = nil)
215
+ set_or_return(
216
+ :name,
217
+ arg,
218
+ :kind_of => [ String ]
219
+ )
220
+ end
221
+
222
+ # Adds a supported platform, with version checking strings.
223
+ #
224
+ # === Parameters
225
+ # platform<String>,<Symbol>:: The platform (like :ubuntu or :mac_os_x)
226
+ # version<String>:: A version constraint of the form "OP VERSION",
227
+ # where OP is one of < <= = > >= ~> and VERSION has
228
+ # the form x.y.z or x.y.
229
+ #
230
+ # === Returns
231
+ # versions<Array>:: Returns the list of versions for the platform
232
+ def supports(platform, *version_args)
233
+ version = version_args.first
234
+ @platforms[platform] = Solve::Constraint.new(version).to_s
235
+ @platforms[platform]
236
+ rescue Solve::Errors::InvalidConstraintFormat => ex
237
+ raise InvalidVersionConstraint, ex.to_s
238
+ end
239
+
240
+ # Adds a dependency on another cookbook, with version checking strings.
241
+ #
242
+ # === Parameters
243
+ # cookbook<String>:: The cookbook
244
+ # version<String>:: A version constraint of the form "OP VERSION",
245
+ # where OP is one of < <= = > >= ~> and VERSION has
246
+ # the form x.y.z or x.y.
247
+ #
248
+ # === Returns
249
+ # versions<Array>:: Returns the list of versions for the platform
250
+ def depends(cookbook, *version_args)
251
+ version = version_args.first
252
+ @dependencies[cookbook] = Solve::Constraint.new(version).to_s
253
+ @dependencies[cookbook]
254
+ rescue Solve::Errors::InvalidConstraintFormat => ex
255
+ raise InvalidVersionConstraint, ex.to_s
256
+ end
257
+
258
+ # Adds a recommendation for another cookbook, with version checking strings.
259
+ #
260
+ # === Parameters
261
+ # cookbook<String>:: The cookbook
262
+ # version<String>:: A version constraint of the form "OP VERSION",
263
+ # where OP is one of < <= = > >= ~> and VERSION has
264
+ # the form x.y.z or x.y.
265
+ #
266
+ # === Returns
267
+ # versions<Array>:: Returns the list of versions for the platform
268
+ def recommends(cookbook, *version_args)
269
+ version = version_args.first
270
+ @recommendations[cookbook] = Solve::Constraint.new(version).to_s
271
+ @recommendations[cookbook]
272
+ rescue Solve::Errors::InvalidConstraintFormat => ex
273
+ raise InvalidVersionConstraint, ex.to_s
274
+ end
275
+
276
+ # Adds a suggestion for another cookbook, with version checking strings.
277
+ #
278
+ # === Parameters
279
+ # cookbook<String>:: The cookbook
280
+ # version<String>:: A version constraint of the form "OP VERSION",
281
+ # where OP is one of < <= = > >= ~> and VERSION has the
282
+ # formx.y.z or x.y.
283
+ #
284
+ # === Returns
285
+ # versions<Array>:: Returns the list of versions for the platform
286
+ def suggests(cookbook, *version_args)
287
+ version = version_args.first
288
+ @suggestions[cookbook] = Solve::Constraint.new(version).to_s
289
+ @suggestions[cookbook]
290
+ rescue Solve::Errors::InvalidConstraintFormat => ex
291
+ raise InvalidVersionConstraint, ex.to_s
292
+ end
293
+
294
+ # Adds a conflict for another cookbook, with version checking strings.
295
+ #
296
+ # === Parameters
297
+ # cookbook<String>:: The cookbook
298
+ # version<String>:: A version constraint of the form "OP VERSION",
299
+ # where OP is one of < <= = > >= ~> and VERSION has
300
+ # the form x.y.z or x.y.
301
+ #
302
+ # === Returns
303
+ # versions<Array>:: Returns the list of versions for the platform
304
+ def conflicts(cookbook, *version_args)
305
+ version = version_args.first
306
+ @conflicting[cookbook] = Solve::Constraint.new(version).to_s
307
+ @conflicting[cookbook]
308
+ rescue Solve::Errors::InvalidConstraintFormat => ex
309
+ raise InvalidVersionConstraint, ex.to_s
310
+ end
311
+
312
+ # Adds a recipe, definition, or resource provided by this cookbook.
313
+ #
314
+ # Recipes are specified as normal
315
+ # Definitions are followed by (), and can include :params for prototyping
316
+ # Resources are the stringified version (service[apache2])
317
+ #
318
+ # === Parameters
319
+ # recipe, definition, resource<String>:: The thing we provide
320
+ # version<String>:: A version constraint of the form "OP VERSION",
321
+ # where OP is one of < <= = > >= ~> and VERSION has
322
+ # the form x.y.z or x.y.
323
+ #
324
+ # === Returns
325
+ # versions<Array>:: Returns the list of versions for the platform
326
+ def provides(cookbook, *version_args)
327
+ version = version_args.first
328
+ @providing[cookbook] = Solve::Constraint.new(version).to_s
329
+ @providing[cookbook]
330
+ rescue Solve::Errors::InvalidConstraintFormat => ex
331
+ raise InvalidVersionConstraint, ex.to_s
332
+ end
333
+
334
+ # Adds a cookbook that is replaced by this one, with version checking strings.
335
+ #
336
+ # === Parameters
337
+ # cookbook<String>:: The cookbook we replace
338
+ # version<String>:: A version constraint of the form "OP VERSION",
339
+ # where OP is one of < <= = > >= ~> and VERSION has the form x.y.z or x.y.
340
+ #
341
+ # === Returns
342
+ # versions<Array>:: Returns the list of versions for the platform
343
+ def replaces(cookbook, *version_args)
344
+ version = version_args.first
345
+ @replacing[cookbook] = Solve::Constraint.new(version).to_s
346
+ @replacing[cookbook]
347
+ rescue Solve::Errors::InvalidConstraintFormat => ex
348
+ raise InvalidVersionConstraint, ex.to_s
349
+ end
350
+
351
+ # Adds a description for a recipe.
352
+ #
353
+ # === Parameters
354
+ # recipe<String>:: The recipe
355
+ # description<String>:: The description of the recipe
356
+ #
357
+ # === Returns
358
+ # description<String>:: Returns the current description
359
+ def recipe(name, description)
360
+ @recipes[name] = description
361
+ end
362
+
363
+ # Adds an attribute )hat a user needs to configure for this cookbook. Takes
364
+ # a name (with the / notation for a nested attribute), followed by any of
365
+ # these options
366
+ #
367
+ # display_name<String>:: What a UI should show for this attribute
368
+ # description<String>:: A hint as to what this attr is for
369
+ # choice<Array>:: An array of choices to present to the user.
370
+ # calculated<Boolean>:: If true, the default value is calculated by the recipe and cannot be displayed.
371
+ # type<String>:: "string" or "array" - default is "string" ("hash" is supported for backwards compatibility)
372
+ # required<String>:: Whether this attr is 'required', 'recommended' or 'optional' - default 'optional' (true/false values also supported for backwards compatibility)
373
+ # recipes<Array>:: An array of recipes which need this attr set.
374
+ # default<String>,<Array>,<Hash>:: The default value
375
+ #
376
+ # === Parameters
377
+ # name<String>:: The name of the attribute ('foo', or 'apache2/log_dir')
378
+ # options<Hash>:: The description of the options
379
+ #
380
+ # === Returns
381
+ # options<Hash>:: Returns the current options hash
382
+ def attribute(name, options)
383
+ validate(
384
+ options,
385
+ {
386
+ :display_name => { :kind_of => String },
387
+ :description => { :kind_of => String },
388
+ :choice => { :kind_of => [ Array ], :default => [] },
389
+ :calculated => { :equal_to => [ true, false ], :default => false },
390
+ :type => { :equal_to => [ "string", "array", "hash", "symbol" ], :default => "string" },
391
+ :required => { :equal_to => [ "required", "recommended", "optional", true, false ], :default => "optional" },
392
+ :recipes => { :kind_of => [ Array ], :default => [] },
393
+ :default => { :kind_of => [ String, Array, Hash ] }
394
+ }
395
+ )
396
+ options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil?
397
+ validate_string_array(options[:choice])
398
+ validate_calculated_default_rule(options)
399
+ validate_choice_default_rule(options)
400
+
401
+ @attributes[name] = options
402
+ @attributes[name]
403
+ end
404
+
405
+ def grouping(name, options)
406
+ validate(
407
+ options,
408
+ {
409
+ :title => { :kind_of => String },
410
+ :description => { :kind_of => String }
411
+ }
412
+ )
413
+ @groupings[name] = options
414
+ @groupings[name]
415
+ end
416
+
417
+ def to_hash
418
+ {
419
+ NAME => self.name,
420
+ DESCRIPTION => self.description,
421
+ LONG_DESCRIPTION => self.long_description,
422
+ MAINTAINER => self.maintainer,
423
+ MAINTAINER_EMAIL => self.maintainer_email,
424
+ LICENSE => self.license,
425
+ PLATFORMS => self.platforms,
426
+ DEPENDENCIES => self.dependencies,
427
+ RECOMMENDATIONS => self.recommendations,
428
+ SUGGESTIONS => self.suggestions,
429
+ CONFLICTING => self.conflicting,
430
+ PROVIDING => self.providing,
431
+ REPLACING => self.replacing,
432
+ ATTRIBUTES => self.attributes,
433
+ GROUPINGS => self.groupings,
434
+ RECIPES => self.recipes,
435
+ VERSION => self.version
436
+ }
437
+ end
438
+
439
+ def from_hash(o)
440
+ @name = o[NAME] if o.has_key?(NAME)
441
+ @description = o[DESCRIPTION] if o.has_key?(DESCRIPTION)
442
+ @long_description = o[LONG_DESCRIPTION] if o.has_key?(LONG_DESCRIPTION)
443
+ @maintainer = o[MAINTAINER] if o.has_key?(MAINTAINER)
444
+ @maintainer_email = o[MAINTAINER_EMAIL] if o.has_key?(MAINTAINER_EMAIL)
445
+ @license = o[LICENSE] if o.has_key?(LICENSE)
446
+ @platforms = o[PLATFORMS] if o.has_key?(PLATFORMS)
447
+ @dependencies = handle_deprecated_constraints(o[DEPENDENCIES]) if o.has_key?(DEPENDENCIES)
448
+ @recommendations = handle_deprecated_constraints(o[RECOMMENDATIONS]) if o.has_key?(RECOMMENDATIONS)
449
+ @suggestions = handle_deprecated_constraints(o[SUGGESTIONS]) if o.has_key?(SUGGESTIONS)
450
+ @conflicting = handle_deprecated_constraints(o[CONFLICTING]) if o.has_key?(CONFLICTING)
451
+ @providing = o[PROVIDING] if o.has_key?(PROVIDING)
452
+ @replacing = handle_deprecated_constraints(o[REPLACING]) if o.has_key?(REPLACING)
453
+ @attributes = o[ATTRIBUTES] if o.has_key?(ATTRIBUTES)
454
+ @groupings = o[GROUPINGS] if o.has_key?(GROUPINGS)
455
+ @recipes = o[RECIPES] if o.has_key?(RECIPES)
456
+ @version = o[VERSION] if o.has_key?(VERSION)
457
+ self
458
+ end
459
+
460
+ def from_file(filepath)
461
+ super
462
+ rescue IOError => ex
463
+ raise Berkshelf::CookbookNotFound, ex.to_s
464
+ end
465
+
466
+ private
467
+
468
+ # Verify that the given array is an array of strings
469
+ #
470
+ # Raise an exception if the members of the array are not Strings
471
+ #
472
+ # === Parameters
473
+ # arry<Array>:: An array to be validated
474
+ def validate_string_array(arry)
475
+ if arry.kind_of?(Array)
476
+ arry.each do |choice|
477
+ validate( {:choice => choice}, {:choice => {:kind_of => String}} )
478
+ end
479
+ end
480
+ end
481
+
482
+ # For backwards compatibility, remap Boolean values to String
483
+ # true is mapped to "required"
484
+ # false is mapped to "optional"
485
+ #
486
+ # === Parameters
487
+ # required_attr<String><Boolean>:: The value of options[:required]
488
+ #
489
+ # === Returns
490
+ # required_attr<String>:: "required", "recommended", or "optional"
491
+ def remap_required_attribute(value)
492
+ case value
493
+ when true
494
+ value = "required"
495
+ when false
496
+ value = "optional"
497
+ end
498
+ value
499
+ end
500
+
501
+ def validate_calculated_default_rule(options)
502
+ calculated_conflict = ((options[:default].is_a?(Array) && !options[:default].empty?) ||
503
+ (options[:default].is_a?(String) && !options[:default] != "")) &&
504
+ options[:calculated] == true
505
+ raise ArgumentError, "Default cannot be specified if calculated is true!" if calculated_conflict
506
+ end
507
+
508
+ def validate_choice_default_rule(options)
509
+ return if !options[:choice].is_a?(Array) || options[:choice].empty?
510
+
511
+ if options[:default].is_a?(String) && options[:default] != ""
512
+ raise ArgumentError, "Default must be one of your choice values!" if options[:choice].index(options[:default]) == nil
513
+ end
514
+
515
+ if options[:default].is_a?(Array) && !options[:default].empty?
516
+ options[:default].each do |val|
517
+ raise ArgumentError, "Default values must be a subset of your choice values!" if options[:choice].index(val) == nil
518
+ end
519
+ end
520
+ end
521
+
522
+ # This method translates version constraint strings from
523
+ # cookbooks with the old format.
524
+ #
525
+ # Before we began respecting version constraints, we allowed
526
+ # multiple constraints to be placed on cookbooks, as well as the
527
+ # << and >> operators, which are now just < and >. For
528
+ # specifications with more than one constraint, we return an
529
+ # empty array (otherwise, we're silently abiding only part of
530
+ # the contract they have specified to us). If there is only one
531
+ # constraint, we are replacing the old << and >> with the new <
532
+ # and >.
533
+ def handle_deprecated_constraints(specification)
534
+ specification.inject(Hashie::Mash.new) do |acc, (cb, constraints)|
535
+ constraints = Array(constraints)
536
+ acc[cb] = (constraints.empty? || constraints.size > 1) ? [] : constraints.first.gsub(/>>/, '>').gsub(/<</, '<')
537
+ acc
538
+ end
539
+ end
540
+ end
541
+
542
+ #== Chef::Cookbook::MinimalMetadata
543
+ # MinimalMetadata is a duck type of Cookbook::Metadata, used
544
+ # internally by Chef Server when determining the optimal set of
545
+ # cookbooks for a node.
546
+ #
547
+ # MinimalMetadata objects typically contain only enough information
548
+ # to solve the cookbook collection for a run list, but not enough to
549
+ # generate the proper response
550
+ class MinimalMetadata < Metadata
551
+ def initialize(name, params)
552
+ @name = name
553
+ from_hash(params)
554
+ end
555
+ end
556
+ end