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,206 @@
1
+ module Berkshelf
2
+ class CookbookSource
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ class SiteLocation
5
+ include Location
6
+
7
+ attr_reader :api_uri
8
+ attr_accessor :version_constraint
9
+
10
+ OPSCODE_COMMUNITY_API = 'http://cookbooks.opscode.com/api/v1/cookbooks'.freeze
11
+
12
+ class << self
13
+ # @param [String] target
14
+ # file path to the tar.gz archive on disk
15
+ # @param [String] destination
16
+ # file path to extract the contents of the target to
17
+ def unpack(target, destination)
18
+ Archive::Tar::Minitar.unpack(Zlib::GzipReader.new(File.open(target, 'rb')), destination)
19
+ end
20
+
21
+ # @param [DepSelector::VersionConstraint] constraint
22
+ # version constraint to solve for
23
+ #
24
+ # @param [Hash] versions
25
+ # a hash where the keys are a DepSelector::Version representing a Cookbook version
26
+ # number and their values are the URI the Cookbook of the corrosponding version can
27
+ # be downloaded from. This format is also the output of the #versions function on
28
+ # instances of this class.
29
+ #
30
+ # Example:
31
+ # {
32
+ # 0.101.2 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_2",
33
+ # 0.101.0 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_0",
34
+ # 0.100.2 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_100_2",
35
+ # 0.100.0 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_100_0"
36
+ # }
37
+ #
38
+ # @return [Array]
39
+ # an array where the first element is a DepSelector::Version representing the best version
40
+ # for the given constraint and the second element is the URI to where the corrosponding
41
+ # version of the Cookbook can be downloaded from
42
+ #
43
+ # Example:
44
+ # [ 0.101.2 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_2" ]
45
+ def solve_for_constraint(constraint, versions)
46
+ versions.each do |version, uri|
47
+ if constraint.include?(version)
48
+ return [ version, uri ]
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def initialize(name, options = {})
55
+ options[:site] ||= OPSCODE_COMMUNITY_API
56
+
57
+ @name = name
58
+ @version_constraint = options[:version_constraint]
59
+ @api_uri = options[:site]
60
+ end
61
+
62
+ def download(destination)
63
+ version, uri = target_version
64
+ remote_file = rest.get_rest(uri)['file']
65
+ downloaded_tf = rest.get_rest(remote_file, true)
66
+
67
+ dir = Dir.mktmpdir
68
+ cb_path = File.join(destination, "#{name}-#{version}")
69
+
70
+ self.class.unpack(downloaded_tf.path, dir)
71
+ FileUtils.mv(File.join(dir, name), cb_path, :force => true)
72
+
73
+ cb_path
74
+ end
75
+
76
+ # @return [Array]
77
+ # an Array where the first element is a DepSelector::Version representing the latest version of
78
+ # the Cookbook and the second element is the URI to where the corrosponding version of the
79
+ # Cookbook can be downloaded from
80
+ #
81
+ # Example:
82
+ # [ 0.101.2, "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_2" ]
83
+ def version(version_string)
84
+ quietly {
85
+ result = rest.get_rest("#{name}/versions/#{uri_escape_version(version_string)}")
86
+ dep_ver = DepSelector::Version.new(result['version'])
87
+
88
+ [ dep_ver, result['file'] ]
89
+ }
90
+ rescue Net::HTTPServerException => e
91
+ if e.response.code == "404"
92
+ raise CookbookNotFound, "Cookbook name: '#{name}' version: '#{version_string}' not found at site: '#{api_uri}'"
93
+ else
94
+ raise
95
+ end
96
+ end
97
+
98
+ # @return [Hash]
99
+ # a hash where the keys are a DepSelector::Version representing a Cookbook version
100
+ # number and their values are the URI the Cookbook of the corrosponding version can
101
+ # be downloaded from
102
+ #
103
+ # Example:
104
+ # {
105
+ # 0.101.2 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_2",
106
+ # 0.101.0 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_0",
107
+ # 0.100.2 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_100_2",
108
+ # 0.100.0 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_100_0"
109
+ # }
110
+ def versions
111
+ versions = Hash.new
112
+ quietly {
113
+ rest.get_rest(name)['versions'].each do |uri|
114
+ version_string = version_from_uri(File.basename(uri))
115
+ version = DepSelector::Version.new(version_string)
116
+
117
+ versions[version] = uri
118
+ end
119
+ }
120
+
121
+ versions
122
+ rescue Net::HTTPServerException => e
123
+ if e.response.code == "404"
124
+ raise CookbookNotFound, "Cookbook '#{name}' not found at site: '#{api_uri}'"
125
+ else
126
+ raise
127
+ end
128
+ end
129
+
130
+ # @return [Array]
131
+ # an array where the first element is a DepSelector::Version representing the latest version of
132
+ # the Cookbook and the second element is the URI to where the corrosponding version of the
133
+ # Cookbook can be downloaded from
134
+ #
135
+ # Example:
136
+ # [ 0.101.2 => "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_2" ]
137
+ def latest_version
138
+ quietly {
139
+ uri = rest.get_rest(name)['latest_version']
140
+ version_string = version_from_uri(uri)
141
+ dep_ver = DepSelector::Version.new(version_string)
142
+
143
+ [ dep_ver, uri ]
144
+ }
145
+ rescue Net::HTTPServerException => e
146
+ if e.response.code == "404"
147
+ raise CookbookNotFound, "Cookbook '#{name}' not found at site: '#{api_uri}'"
148
+ else
149
+ raise
150
+ end
151
+ end
152
+
153
+ def api_uri=(uri)
154
+ @rest = nil
155
+ @api_uri = uri
156
+ end
157
+
158
+ def to_s
159
+ "site: '#{api_uri}'"
160
+ end
161
+
162
+ private
163
+
164
+ def rest
165
+ @rest ||= Chef::REST.new(api_uri, false, false)
166
+ end
167
+
168
+ def uri_escape_version(version)
169
+ version.gsub('.', '_')
170
+ end
171
+
172
+ def version_from_uri(latest_version)
173
+ File.basename(latest_version).gsub('_', '.')
174
+ end
175
+
176
+ # @return [Array]
177
+ # an Array where the first element is a DepSelector::Version and the second element is
178
+ # the URI to where the corrosponding version of the Cookbook can be downloaded from.
179
+ #
180
+ #
181
+ # The version is determined by the value of the version_constraint attribute of this
182
+ # instance of SiteLocation:
183
+ #
184
+ # If it is not set: the latest_version of the Cookbook will be returned
185
+ #
186
+ # If it is set: the return value will be determined by the version_constraint and the
187
+ # available versions will be solved
188
+ #
189
+ # Example:
190
+ # [ 0.101.2, "http://cookbooks.opscode.com/api/v1/cookbooks/nginx/versions/0_101_2" ]
191
+ def target_version
192
+ if version_constraint
193
+ solution = self.class.solve_for_constraint(version_constraint, versions)
194
+
195
+ unless solution
196
+ raise NoSolution, "Could not satisfy version constraint: #{version_constraint} for Cookbook '#{name}'"
197
+ end
198
+
199
+ solution
200
+ else
201
+ latest_version
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,77 @@
1
+ require 'fileutils'
2
+
3
+ module Berkshelf
4
+ # @author Jamie Winsor <jamie@vialstudios.com>
5
+ class CookbookStore
6
+ attr_reader :storage_path
7
+
8
+ # Create a new instance of CookbookStore with the given
9
+ # storage_path.
10
+ #
11
+ # @param [String] storage_path
12
+ # local filesystem path to the location to be initialized
13
+ # as a CookbookStore.
14
+ def initialize(storage_path)
15
+ @storage_path = Pathname.new(storage_path)
16
+ initialize_filesystem
17
+ end
18
+
19
+ # Returns an instance of CachedCookbook representing the
20
+ # Cookbook of your given name and version.
21
+ #
22
+ # @param [String] name
23
+ # name of the Cookbook you want to retrieve
24
+ # @param [String] version
25
+ # version of the Cookbook you want to retrieve
26
+ #
27
+ # @return [Berkshelf::CachedCookbook]
28
+ def cookbook(name, version)
29
+ return nil unless downloaded?(name, version)
30
+
31
+ path = cookbook_path(name, version)
32
+ CachedCookbook.from_path(path)
33
+ end
34
+
35
+ # Returns an array of all of the Cookbooks that have been cached
36
+ # to the storage_path of this instance of CookbookStore.
37
+ #
38
+ # @return [Array<Berkshelf::CachedCookbook>]
39
+ def cookbooks
40
+ [].tap do |cookbooks|
41
+ storage_path.each_child do |p|
42
+ cached_cookbook = CachedCookbook.from_path(p)
43
+
44
+ cookbooks << cached_cookbook if cached_cookbook
45
+ end
46
+ end
47
+ end
48
+
49
+ # Returns an expanded path to the location on disk where the Cookbook
50
+ # of the given name and version is located.
51
+ #
52
+ # @param [String] name
53
+ # @param [String] version
54
+ #
55
+ # @return [Pathname]
56
+ def cookbook_path(name, version)
57
+ storage_path.join("#{name}-#{version}")
58
+ end
59
+
60
+ # Returns true if the Cookbook of the given name and verion is downloaded
61
+ # to this instance of CookbookStore.
62
+ #
63
+ # @param [String] name
64
+ # @param [String] version
65
+ #
66
+ # @return [Boolean]
67
+ def downloaded?(name, version)
68
+ cookbook_path(name, version).cookbook?
69
+ end
70
+
71
+ private
72
+
73
+ def initialize_filesystem
74
+ FileUtils.mkdir_p(storage_path, :mode => 0755)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.each do |path|
2
+ require "berkshelf/core_ext/#{File.basename(path, '.rb')}"
3
+ end
@@ -0,0 +1,14 @@
1
+ class File
2
+ class << self
3
+ # Returns true or false if the given path is a Chef Cookbook
4
+ #
5
+ # @param [#to_s] path
6
+ # path of directory to reflect on
7
+ #
8
+ # @return [Boolean]
9
+ def cookbook?(path)
10
+ File.exists?(File.join(path, "metadata.rb"))
11
+ end
12
+ alias_method :chef_cookbook?, :cookbook?
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # From ActiveSupport: https://github.com/rails/rails/blob/c2c8ef57d6f00d1c22743dc43746f95704d67a95/activesupport/lib/active_support/core_ext/kernel/reporting.rb#L39
2
+
3
+ require 'rbconfig'
4
+
5
+ module Kernel
6
+ # Silences any stream for the duration of the block.
7
+ #
8
+ # silence_stream(STDOUT) do
9
+ # puts 'This will never be seen'
10
+ # end
11
+ #
12
+ # puts 'But this will'
13
+ def silence_stream(stream)
14
+ old_stream = stream.dup
15
+ stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
16
+ stream.sync = true
17
+ yield
18
+ ensure
19
+ stream.reopen(old_stream)
20
+ end
21
+
22
+ # Silences both STDOUT and STDERR, even for subprocesses.
23
+ #
24
+ # quietly { system 'bundle install' }
25
+ #
26
+ def quietly
27
+ silence_stream(STDOUT) do
28
+ silence_stream(STDERR) do
29
+ yield
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ class Pathname
2
+ # Returns true or false if the path of the instantiated
3
+ # Pathname or a parent directory contains a Tryhard Pack
4
+ # data directory.
5
+ #
6
+ # @return [Boolean]
7
+ def cookbook?
8
+ self.join('metadata.rb').exist?
9
+ end
10
+ alias_method :chef_cookbook?, :cookbook?
11
+
12
+ # Ascend the directory structure from the given path to find a
13
+ # the root of a Chef Cookbook. If no Cookbook is found, nil is returned
14
+ #
15
+ # @return [Pathname, nil]
16
+ def cookbook_root
17
+ self.ascend do |potential_root|
18
+ if potential_root.cookbook?
19
+ return potential_root
20
+ end
21
+ end
22
+ end
23
+ alias_method :chef_cookbook_root, :cookbook_root
24
+ end
@@ -0,0 +1,92 @@
1
+ module Berkshelf
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class Downloader
4
+ extend Forwardable
5
+
6
+ attr_reader :cookbook_store
7
+ attr_reader :queue
8
+
9
+ def_delegators :@cookbook_store, :storage_path
10
+
11
+ def initialize(cookbook_store)
12
+ @cookbook_store = cookbook_store
13
+ @queue = []
14
+ end
15
+
16
+ # Add a CookbookSource to the downloader's queue
17
+ #
18
+ # @param [Berkshelf::CookbookSource] source
19
+ #
20
+ # @return [Array<Berkshelf::CookbookSource>]
21
+ # the queue - an array of Berkshelf::CookbookSources
22
+ def enqueue(source)
23
+ unless validate_source(source)
24
+ raise ArgumentError, "Invalid CookbookSource: can only enqueue valid instances of CookbookSource."
25
+ end
26
+
27
+ @queue << source
28
+ end
29
+
30
+ # Remove a CookbookSource from the downloader's queue
31
+ #
32
+ # @param [Berkshelf::CookbookSource] source
33
+ #
34
+ # @return [Berkshelf::CookbookSource]
35
+ # the CookbookSource removed from the queue
36
+ def dequeue(source)
37
+ @queue.delete(source)
38
+ end
39
+
40
+ # Download each CookbookSource in the queue. Upon successful download
41
+ # of a CookbookSource it is removed from the queue. If a CookbookSource
42
+ # fails to download it remains in the queue.
43
+ #
44
+ # @return [TXResultSet]
45
+ def download_all
46
+ results = TXResultSet.new
47
+
48
+ queue.each do |source|
49
+ results.add_result download(source)
50
+ end
51
+
52
+ results.success.each { |result| dequeue(result.source) }
53
+
54
+ results
55
+ end
56
+
57
+ # Downloads the given CookbookSource
58
+ #
59
+ # @param [CookbookSource] source
60
+ # the source to download
61
+ #
62
+ # @return [TXResult]
63
+ def download(source)
64
+ status, message = source.download(storage_path)
65
+ TXResult.new(status, message, source)
66
+ end
67
+
68
+ # Downloads the given CookbookSource. Raises a DownloadFailure error
69
+ # if the download was not successful.
70
+ #
71
+ # @param [CookbookSource] source
72
+ # the source to download
73
+ #
74
+ # @return [TXResult]
75
+ def download!(source)
76
+ result = download(source)
77
+ raise DownloadFailure, result.message if result.failed?
78
+
79
+ result
80
+ end
81
+
82
+ def downloaded?(source)
83
+ source.downloaded? || cookbook_store.downloaded?(source.name, source.local_version)
84
+ end
85
+
86
+ private
87
+
88
+ def validate_source(source)
89
+ source.is_a?(Berkshelf::CookbookSource)
90
+ end
91
+ end
92
+ end