berkshelf 0.1.1

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