berkshelf 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. data/.gitignore +20 -0
  2. data/.rbenv-version +1 -0
  3. data/Gemfile +3 -0
  4. data/Guardfile +23 -0
  5. data/LICENSE +22 -0
  6. data/README.rdoc +102 -0
  7. data/Thorfile +47 -0
  8. data/berkshelf.gemspec +39 -0
  9. data/features/config.sample.yml +3 -0
  10. data/features/init_command.feature +40 -0
  11. data/features/install.feature +55 -0
  12. data/features/lockfile.feature +22 -0
  13. data/features/step_definitions/chef_server_steps.rb +13 -0
  14. data/features/step_definitions/cli_steps.rb +56 -0
  15. data/features/step_definitions/filesystem_steps.rb +79 -0
  16. data/features/support/env.rb +55 -0
  17. data/features/update.feature +23 -0
  18. data/features/upload_command.feature +43 -0
  19. data/features/without.feature +25 -0
  20. data/lib/berkshelf.rb +90 -0
  21. data/lib/berkshelf/berksfile.rb +170 -0
  22. data/lib/berkshelf/cached_cookbook.rb +253 -0
  23. data/lib/berkshelf/cookbook_source.rb +139 -0
  24. data/lib/berkshelf/cookbook_source/git_location.rb +54 -0
  25. data/lib/berkshelf/cookbook_source/path_location.rb +27 -0
  26. data/lib/berkshelf/cookbook_source/site_location.rb +206 -0
  27. data/lib/berkshelf/cookbook_store.rb +77 -0
  28. data/lib/berkshelf/core_ext.rb +3 -0
  29. data/lib/berkshelf/core_ext/file.rb +14 -0
  30. data/lib/berkshelf/core_ext/kernel.rb +33 -0
  31. data/lib/berkshelf/core_ext/pathname.rb +24 -0
  32. data/lib/berkshelf/downloader.rb +92 -0
  33. data/lib/berkshelf/dsl.rb +39 -0
  34. data/lib/berkshelf/errors.rb +20 -0
  35. data/lib/berkshelf/generator_files/Berksfile.erb +3 -0
  36. data/lib/berkshelf/generator_files/chefignore +44 -0
  37. data/lib/berkshelf/git.rb +70 -0
  38. data/lib/berkshelf/init_generator.rb +38 -0
  39. data/lib/berkshelf/lockfile.rb +42 -0
  40. data/lib/berkshelf/resolver.rb +176 -0
  41. data/lib/berkshelf/tx_result.rb +12 -0
  42. data/lib/berkshelf/tx_result_set.rb +37 -0
  43. data/lib/berkshelf/uploader.rb +153 -0
  44. data/lib/berkshelf/version.rb +3 -0
  45. data/lib/chef/knife/berks_init.rb +29 -0
  46. data/lib/chef/knife/berks_install.rb +27 -0
  47. data/lib/chef/knife/berks_update.rb +23 -0
  48. data/lib/chef/knife/berks_upload.rb +39 -0
  49. data/spec/fixtures/Berksfile +3 -0
  50. data/spec/fixtures/cookbooks/example_cookbook-0.5.0/README.md +12 -0
  51. data/spec/fixtures/cookbooks/example_cookbook-0.5.0/metadata.rb +6 -0
  52. data/spec/fixtures/cookbooks/example_cookbook-0.5.0/recipes/default.rb +8 -0
  53. data/spec/fixtures/cookbooks/example_cookbook/README.md +12 -0
  54. data/spec/fixtures/cookbooks/example_cookbook/metadata.rb +6 -0
  55. data/spec/fixtures/cookbooks/example_cookbook/recipes/default.rb +8 -0
  56. data/spec/fixtures/cookbooks/invalid_ruby_files-1.0.0/recipes/default.rb +1 -0
  57. data/spec/fixtures/cookbooks/invalid_template_files-1.0.0/templates/default/broken.erb +1 -0
  58. data/spec/fixtures/cookbooks/nginx-0.100.5/README.md +77 -0
  59. data/spec/fixtures/cookbooks/nginx-0.100.5/attributes/default.rb +65 -0
  60. data/spec/fixtures/cookbooks/nginx-0.100.5/definitions/nginx_site.rb +35 -0
  61. data/spec/fixtures/cookbooks/nginx-0.100.5/files/default/mime.types +73 -0
  62. data/spec/fixtures/cookbooks/nginx-0.100.5/files/ubuntu/mime.types +73 -0
  63. data/spec/fixtures/cookbooks/nginx-0.100.5/libraries/nginxlib.rb +1 -0
  64. data/spec/fixtures/cookbooks/nginx-0.100.5/metadata.rb +91 -0
  65. data/spec/fixtures/cookbooks/nginx-0.100.5/providers/defprovider.rb +1 -0
  66. data/spec/fixtures/cookbooks/nginx-0.100.5/recipes/default.rb +59 -0
  67. data/spec/fixtures/cookbooks/nginx-0.100.5/resources/defresource.rb +1 -0
  68. data/spec/fixtures/cookbooks/nginx-0.100.5/templates/default/nginx.pill.erb +15 -0
  69. data/spec/fixtures/cookbooks/nginx-0.100.5/templates/default/plugins/nginx.rb.erb +66 -0
  70. data/spec/fixtures/lockfile_spec/with_lock/Berksfile +1 -0
  71. data/spec/fixtures/lockfile_spec/without_lock/Berksfile.lock +5 -0
  72. data/spec/spec_helper.rb +92 -0
  73. data/spec/support/chef_api.rb +27 -0
  74. data/spec/support/matchers/file_system_matchers.rb +115 -0
  75. data/spec/support/matchers/filepath_matchers.rb +19 -0
  76. data/spec/unit/berkshelf/cached_cookbook_spec.rb +420 -0
  77. data/spec/unit/berkshelf/cookbook_source/git_location_spec.rb +59 -0
  78. data/spec/unit/berkshelf/cookbook_source/path_location_spec.rb +34 -0
  79. data/spec/unit/berkshelf/cookbook_source/site_location_spec.rb +166 -0
  80. data/spec/unit/berkshelf/cookbook_source_spec.rb +194 -0
  81. data/spec/unit/berkshelf/cookbook_store_spec.rb +71 -0
  82. data/spec/unit/berkshelf/cookbookfile_spec.rb +160 -0
  83. data/spec/unit/berkshelf/downloader_spec.rb +82 -0
  84. data/spec/unit/berkshelf/dsl_spec.rb +42 -0
  85. data/spec/unit/berkshelf/git_spec.rb +63 -0
  86. data/spec/unit/berkshelf/init_generator_spec.rb +52 -0
  87. data/spec/unit/berkshelf/lockfile_spec.rb +25 -0
  88. data/spec/unit/berkshelf/resolver_spec.rb +126 -0
  89. data/spec/unit/berkshelf/tx_result_set_spec.rb +77 -0
  90. data/spec/unit/berkshelf/tx_result_spec.rb +21 -0
  91. data/spec/unit/berkshelf/uploader_spec.rb +71 -0
  92. data/spec/unit/berkshelf_spec.rb +29 -0
  93. metadata +411 -0
@@ -0,0 +1,253 @@
1
+ require 'chef/checksum_cache'
2
+ require 'chef/cookbook/syntax_check'
3
+
4
+ module Berkshelf
5
+ # @author Jamie Winsor <jamie@vialstudios.com>
6
+ class CachedCookbook
7
+ class << self
8
+ # @param [String] path
9
+ # a path on disk to the location of a Cookbook downloaded by the Downloader
10
+ #
11
+ # @return [CachedCookbook]
12
+ # an instance of CachedCookbook initialized by the contents found at the
13
+ # given path.
14
+ def from_path(path)
15
+ matchdata = File.basename(path.to_s).match(DIRNAME_REGEXP)
16
+ return nil if matchdata.nil?
17
+
18
+ cached_name = matchdata[1]
19
+
20
+ metadata = Chef::Cookbook::Metadata.new
21
+
22
+ if path.join("metadata.rb").exist?
23
+ metadata.from_file(path.join("metadata.rb").to_s)
24
+ end
25
+
26
+ new(cached_name, path, metadata)
27
+ end
28
+
29
+ # @param [String] filepath
30
+ # a path on disk to the location of a file to checksum
31
+ #
32
+ # @return [String]
33
+ # a checksum that can be used to uniquely identify the file understood
34
+ # by a Chef Server.
35
+ def checksum(filepath)
36
+ Chef::ChecksumCache.generate_md5_checksum_for_file(filepath)
37
+ end
38
+ end
39
+
40
+ DIRNAME_REGEXP = /^(.+)-(\d+\.\d+\.\d+)$/
41
+ CHEF_TYPE = "cookbook_version".freeze
42
+ CHEF_JSON_CLASS = "Chef::CookbookVersion".freeze
43
+
44
+ extend Forwardable
45
+
46
+ attr_reader :cookbook_name
47
+ attr_reader :path
48
+ attr_reader :metadata
49
+
50
+ # @return [Mash]
51
+ # a Mash containing Cookbook file category names as keys and an Array of Hashes
52
+ # containing metadata about the files belonging to that category. This is used
53
+ # to communicate what a Cookbook looks like when uploading to a Chef Server.
54
+ #
55
+ # example:
56
+ # {
57
+ # :recipes => [
58
+ # {
59
+ # name: "default.rb",
60
+ # path: "recipes/default.rb",
61
+ # checksum: "fb1f925dcd5fc4ebf682c4442a21c619",
62
+ # specificity: "default"
63
+ # }
64
+ # ]
65
+ # ...
66
+ # ...
67
+ # }
68
+ attr_reader :manifest
69
+
70
+ def_delegators :@metadata, :version
71
+
72
+ def initialize(name, path, metadata)
73
+ @cookbook_name = name
74
+ @path = path
75
+ @metadata = metadata
76
+ @files = Array.new
77
+ @manifest = Mash.new(
78
+ recipes: Array.new,
79
+ definitions: Array.new,
80
+ libraries: Array.new,
81
+ attributes: Array.new,
82
+ files: Array.new,
83
+ templates: Array.new,
84
+ resources: Array.new,
85
+ providers: Array.new,
86
+ root_files: Array.new
87
+ )
88
+
89
+ load_files
90
+ end
91
+
92
+ # @return [String]
93
+ # the name of the cookbook and the version number separated by a dash (-).
94
+ #
95
+ # example:
96
+ # "nginx-0.101.2"
97
+ def name
98
+ "#{cookbook_name}-#{version}"
99
+ end
100
+
101
+ # @return [Hash]
102
+ # an hash containing the checksums and expanded file paths of all of the
103
+ # files found in the instance of CachedCookbook
104
+ #
105
+ # example:
106
+ # {
107
+ # "da97c94bb6acb2b7900cbf951654fea3" => "/Users/reset/.berkshelf/nginx-0.101.2/README.md"
108
+ # }
109
+ def checksums
110
+ {}.tap do |checksums|
111
+ files.each do |file|
112
+ checksums[self.class.checksum(file)] = file
113
+ end
114
+ end
115
+ end
116
+
117
+ # @param [Symbol] category
118
+ # the category of file to generate metadata about
119
+ # @param [String] target
120
+ # the filepath to the file to get metadata information about
121
+ #
122
+ # @return [Hash]
123
+ # a Hash containing a name, path, checksum, and specificity key representing the
124
+ # metadata about a file contained in a Cookbook. This metadata is used when
125
+ # uploading a Cookbook's files to a Chef Server.
126
+ #
127
+ # example:
128
+ # {
129
+ # name: "default.rb",
130
+ # path: "recipes/default.rb",
131
+ # checksum: "fb1f925dcd5fc4ebf682c4442a21c619",
132
+ # specificity: "default"
133
+ # }
134
+ def file_metadata(category, target)
135
+ target = Pathname.new(target)
136
+
137
+ {
138
+ name: target.basename.to_s,
139
+ path: target.relative_path_from(path).to_s,
140
+ checksum: self.class.checksum(target),
141
+ specificity: file_specificity(category, target)
142
+ }
143
+ end
144
+
145
+ # Validates that this instance of CachedCookbook points to a valid location on disk that
146
+ # contains a cookbook which passes a Ruby and template syntax check. Raises an error if
147
+ # these assertions are not true.
148
+ #
149
+ # @return [Boolean]
150
+ # returns true if Cookbook is valid
151
+ def validate!
152
+ raise CookbookNotFound, "No Cookbook found at: #{path}" unless path.exist?
153
+
154
+ unless quietly { syntax_checker.validate_ruby_files }
155
+ raise CookbookSyntaxError, "Invalid ruby files in cookbook: #{name} (#{version})."
156
+ end
157
+ unless quietly { syntax_checker.validate_templates }
158
+ raise CookbookSyntaxError, "Invalid template files in cookbook: #{name} (#{version})."
159
+ end
160
+
161
+ true
162
+ end
163
+
164
+ def to_hash
165
+ result = manifest.dup
166
+ result['chef_type'] = 'cookbook_version'
167
+ result['name'] = name
168
+ result['cookbook_name'] = cookbook_name
169
+ result['version'] = version
170
+ result['metadata'] = metadata
171
+ result.to_hash
172
+ end
173
+
174
+ def to_json(*a)
175
+ result = self.to_hash
176
+ result['json_class'] = chef_json_class
177
+ result['frozen?'] = false
178
+ result.to_json(*a)
179
+ end
180
+
181
+ private
182
+
183
+ attr_reader :files
184
+
185
+ def chef_type
186
+ CHEF_TYPE
187
+ end
188
+
189
+ def chef_json_class
190
+ CHEF_JSON_CLASS
191
+ end
192
+
193
+ def syntax_checker
194
+ @syntax_checker ||= Chef::Cookbook::SyntaxCheck.new(path.to_s)
195
+ end
196
+
197
+ def load_files
198
+ load_shallow(:recipes, 'recipes', '*.rb')
199
+ load_shallow(:definitions, 'definitions', '*.rb')
200
+ load_shallow(:libraries, 'libraries', '*.rb')
201
+ load_shallow(:attributes, 'attributes', '*.rb')
202
+ load_recursively(:files, "files", "*")
203
+ load_recursively(:templates, "templates", "*")
204
+ load_recursively(:resources, "resources", "*.rb")
205
+ load_recursively(:providers, "providers", "*.rb")
206
+ load_root
207
+ end
208
+
209
+ def load_root
210
+ [].tap do |files|
211
+ Dir.glob(path.join('*'), File::FNM_DOTMATCH).each do |file|
212
+ next if File.directory?(file)
213
+ @files << file
214
+ @manifest[:root_files] << file_metadata(:root_files, file)
215
+ end
216
+ end
217
+ end
218
+
219
+ def load_recursively(category, category_dir, glob)
220
+ [].tap do |files|
221
+ file_spec = path.join(category_dir, '**', glob)
222
+ Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file|
223
+ next if File.directory?(file)
224
+ @files << file
225
+ @manifest[category] << file_metadata(category, file)
226
+ end
227
+ end
228
+ end
229
+
230
+ def load_shallow(category, *path_glob)
231
+ [].tap do |files|
232
+ Dir[path.join(*path_glob)].each do |file|
233
+ @files << file
234
+ @manifest[category] << file_metadata(category, file)
235
+ end
236
+ end
237
+ end
238
+
239
+ # @param [Symbol] category
240
+ # @param [Pathname] target
241
+ #
242
+ # @return [String]
243
+ def file_specificity(category, target)
244
+ case category
245
+ when :files, :templates
246
+ relpath = target.relative_path_from(path).to_s
247
+ relpath.slice(/(.+)\/(.+)\/.+/, 2)
248
+ else
249
+ 'default'
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,139 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class CookbookSource
4
+ module Location
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+
11
+ def download(destination)
12
+ raise NotImplementedError, "Function must be implemented on includer"
13
+ end
14
+ end
15
+
16
+ autoload :SiteLocation, 'berkshelf/cookbook_source/site_location'
17
+ autoload :GitLocation, 'berkshelf/cookbook_source/git_location'
18
+ autoload :PathLocation, 'berkshelf/cookbook_source/path_location'
19
+
20
+ LOCATION_KEYS = [:git, :path, :site]
21
+
22
+ attr_reader :name
23
+ attr_reader :version_constraint
24
+ attr_reader :groups
25
+ attr_reader :location
26
+ attr_reader :local_path
27
+
28
+ # TODO: describe how the options on this function work.
29
+ #
30
+ # @param [String] name
31
+ # @param [String] version_constraint (optional)
32
+ # @param [Hash] options (optional)
33
+ def initialize(*args)
34
+ options = args.last.is_a?(Hash) ? args.pop : {}
35
+ name, constraint = args
36
+
37
+ @name = name
38
+ @version_constraint = DepSelector::VersionConstraint.new(constraint)
39
+ @groups = []
40
+ @local_path = nil
41
+
42
+ if (options.keys & LOCATION_KEYS).length > 1
43
+ raise ArgumentError, "Only one location key (#{LOCATION_KEYS.join(', ')}) may be specified"
44
+ end
45
+
46
+ options[:version_constraint] = version_constraint if version_constraint
47
+
48
+ @location = case
49
+ when options[:git]
50
+ GitLocation.new(name, options)
51
+ when options[:path]
52
+ loc = PathLocation.new(name, options)
53
+ set_local_path loc.path
54
+ loc
55
+ when options[:site]
56
+ SiteLocation.new(name, options)
57
+ else
58
+ SiteLocation.new(name, options)
59
+ end
60
+
61
+ @locked_version = DepSelector::Version.new(options[:locked_version]) if options[:locked_version]
62
+
63
+ add_group(options[:group]) if options[:group]
64
+ add_group(:default) if groups.empty?
65
+ set_downloaded_status(false)
66
+ end
67
+
68
+ def add_group(*groups)
69
+ groups = groups.first if groups.first.is_a?(Array)
70
+ groups.each do |group|
71
+ group = group.to_sym
72
+ @groups << group unless @groups.include?(group)
73
+ end
74
+ end
75
+
76
+ # @param [String] destination
77
+ # destination to download to
78
+ #
79
+ # @return [Array]
80
+ # An array containing the status at index 0 and local path or error message in index 1
81
+ #
82
+ # Example:
83
+ # [ :ok, "/tmp/nginx" ]
84
+ # [ :error, "Cookbook 'sparkle_motion' not found at site: http://cookbooks.opscode.com/api/v1/cookbooks" ]
85
+ def download(destination)
86
+ set_local_path location.download(destination)
87
+ [ :ok, local_path ]
88
+ rescue CookbookNotFound => e
89
+ set_local_path = nil
90
+ [ :error, e.message ]
91
+ end
92
+
93
+ def downloaded?
94
+ !local_path.nil?
95
+ end
96
+
97
+ def metadata
98
+ return nil unless local_path
99
+
100
+ cookbook_metadata = Chef::Cookbook::Metadata.new
101
+ cookbook_metadata.from_file(File.join(local_path, "metadata.rb"))
102
+ cookbook_metadata
103
+ end
104
+
105
+ def to_s
106
+ name
107
+ end
108
+
109
+ def has_group?(group)
110
+ groups.include?(group.to_sym)
111
+ end
112
+
113
+ def dependencies
114
+ return nil unless metadata
115
+
116
+ metadata.dependencies
117
+ end
118
+
119
+ def local_version
120
+ return nil unless metadata
121
+
122
+ metadata.version
123
+ end
124
+
125
+ def locked_version
126
+ @locked_version || local_version
127
+ end
128
+
129
+ private
130
+
131
+ def set_downloaded_status(state)
132
+ @downloaded_state = state
133
+ end
134
+
135
+ def set_local_path(path)
136
+ @local_path = path
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,54 @@
1
+ module Berkshelf
2
+ class CookbookSource
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ class GitLocation
5
+ include Location
6
+
7
+ attr_accessor :uri
8
+ attr_accessor :branch
9
+
10
+ def initialize(name, options)
11
+ @name = name
12
+ @uri = options[:git]
13
+ @branch = options[:branch] || options[:ref] || options[:tag]
14
+ end
15
+
16
+ def download(destination)
17
+ tmp_clone = Dir.mktmpdir
18
+ ::Berkshelf::Git.clone(uri, tmp_clone)
19
+ ::Berkshelf::Git.checkout(tmp_clone, branch) if branch
20
+ unless branch
21
+ self.branch = ::Berkshelf::Git.rev_parse(tmp_clone)
22
+ end
23
+
24
+ unless File.chef_cookbook?(tmp_clone)
25
+ msg = "Cookbook '#{name}' not found at git: #{uri}"
26
+ msg << " with branch '#{branch}'" if branch
27
+ raise CookbookNotFound, msg
28
+ end
29
+
30
+ cb_path = File.join(destination, "#{self.name}-#{self.branch}")
31
+
32
+ FileUtils.mv(tmp_clone, cb_path, :force => true)
33
+
34
+ cb_path
35
+ rescue Berkshelf::GitError
36
+ msg = "Cookbook '#{name}' not found at git: #{uri}"
37
+ msg << " with branch '#{branch}'" if branch
38
+ raise CookbookNotFound, msg
39
+ end
40
+
41
+ def to_s
42
+ s = "git: '#{uri}'"
43
+ s << " with branch '#{branch}" if branch
44
+ s
45
+ end
46
+
47
+ private
48
+
49
+ def git
50
+ @git ||= Berkshelf::Git.new(uri)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ module Berkshelf
2
+ class CookbookSource
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ class PathLocation
5
+ include Location
6
+
7
+ attr_accessor :path
8
+
9
+ def initialize(name, options = {})
10
+ @name = name
11
+ @path = File.expand_path(options[:path])
12
+ end
13
+
14
+ def download(destination)
15
+ unless File.chef_cookbook?(path)
16
+ raise CookbookNotFound, "Cookbook '#{name}' not found at path: '#{path}'"
17
+ end
18
+
19
+ path
20
+ end
21
+
22
+ def to_s
23
+ "path: '#{path}'"
24
+ end
25
+ end
26
+ end
27
+ end