berkshelf 0.4.0 → 0.5.0.rc1

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 (51) hide show
  1. data/berkshelf.gemspec +8 -3
  2. data/bin/berks +1 -1
  3. data/features/groups_install.feature +67 -0
  4. data/features/install.feature +1 -71
  5. data/features/json_formatter.feature +0 -17
  6. data/features/step_definitions/filesystem_steps.rb +1 -3
  7. data/features/update.feature +3 -6
  8. data/features/vendor_install.feature +20 -0
  9. data/generator_files/Vagrantfile.erb +25 -3
  10. data/lib/berkshelf.rb +27 -17
  11. data/lib/berkshelf/base_generator.rb +5 -0
  12. data/lib/berkshelf/berksfile.rb +82 -77
  13. data/lib/berkshelf/cli.rb +37 -26
  14. data/lib/berkshelf/cookbook_source.rb +14 -4
  15. data/lib/berkshelf/errors.rb +4 -1
  16. data/lib/berkshelf/formatters.rb +69 -5
  17. data/lib/berkshelf/formatters/human_readable.rb +26 -8
  18. data/lib/berkshelf/formatters/json.rb +51 -23
  19. data/lib/berkshelf/init_generator.rb +4 -4
  20. data/lib/berkshelf/location.rb +29 -6
  21. data/lib/berkshelf/locations/chef_api_location.rb +23 -5
  22. data/lib/berkshelf/locations/git_location.rb +13 -6
  23. data/lib/berkshelf/locations/path_location.rb +8 -4
  24. data/lib/berkshelf/locations/site_location.rb +9 -3
  25. data/lib/berkshelf/ui.rb +34 -0
  26. data/lib/berkshelf/uploader.rb +37 -103
  27. data/lib/berkshelf/vagrant.rb +65 -0
  28. data/lib/berkshelf/vagrant/action/clean.rb +24 -0
  29. data/lib/berkshelf/vagrant/action/install.rb +47 -0
  30. data/lib/berkshelf/vagrant/action/set_ui.rb +17 -0
  31. data/lib/berkshelf/vagrant/action/upload.rb +40 -0
  32. data/lib/berkshelf/vagrant/config.rb +70 -0
  33. data/lib/berkshelf/vagrant/middleware.rb +52 -0
  34. data/lib/berkshelf/version.rb +1 -1
  35. data/lib/thor/monkies.rb +3 -0
  36. data/lib/thor/monkies/hash_with_indifferent_access.rb +13 -0
  37. data/lib/vagrant_init.rb +2 -0
  38. data/spec/spec_helper.rb +5 -0
  39. data/spec/support/chef_api.rb +6 -1
  40. data/spec/unit/berkshelf/berksfile_spec.rb +93 -55
  41. data/spec/unit/berkshelf/formatters_spec.rb +99 -1
  42. data/spec/unit/berkshelf/init_generator_spec.rb +1 -1
  43. data/spec/unit/berkshelf/location_spec.rb +44 -7
  44. data/spec/unit/berkshelf/lockfile_spec.rb +2 -2
  45. data/spec/unit/berkshelf/uploader_spec.rb +2 -20
  46. data/spec/unit/berkshelf_spec.rb +2 -2
  47. metadata +100 -14
  48. data/features/without.feature +0 -26
  49. data/lib/berkshelf/core_ext/fileutils.rb +0 -90
  50. data/lib/berkshelf/core_ext/kernel.rb +0 -33
  51. data/spec/unit/berkshelf/core_ext/fileutils_spec.rb +0 -20
@@ -4,17 +4,23 @@ module Berkshelf
4
4
  OPSCODE_COMMUNITY_API = 'http://cookbooks.opscode.com/api/v1/cookbooks'.freeze
5
5
 
6
6
  module ClassMethods
7
+ # Returns the location identifier key for the class
8
+ #
9
+ # @return [Symbol]
10
+ attr_reader :location_key
11
+
7
12
  # Register the location key for the including source location with CookbookSource
8
13
  #
9
14
  # @param [Symbol] key
10
- def location_key(key)
15
+ def set_location_key(key)
11
16
  CookbookSource.add_location_key(key, self)
17
+ @location_key = key
12
18
  end
13
19
 
14
20
  # Register a valid option or multiple options with the CookbookSource class
15
21
  #
16
22
  # @param [Symbol] opts
17
- def valid_options(*opts)
23
+ def set_valid_options(*opts)
18
24
  Array(opts).each do |opt|
19
25
  CookbookSource.add_valid_option(opt)
20
26
  end
@@ -125,9 +131,9 @@ module Berkshelf
125
131
  # @param [Solve::Constraint] version_constraint
126
132
  # @param [Hash] options
127
133
  def initialize(name, version_constraint, options = {})
128
- @name = name
134
+ @name = name
129
135
  @version_constraint = version_constraint
130
- @downloaded_status = false
136
+ @downloaded_status = false
131
137
  end
132
138
 
133
139
  # @param [#to_s] destination
@@ -142,22 +148,39 @@ module Berkshelf
142
148
  @downloaded_status
143
149
  end
144
150
 
145
- # Ensures that the given CachedCookbook satisfies the constraint
151
+ # Ensure the retrieved CachedCookbook is valid
146
152
  #
147
153
  # @param [CachedCookbook] cached_cookbook
148
154
  #
149
155
  # @raise [ConstraintNotSatisfied] if the CachedCookbook does not satisfy the version constraint of
150
156
  # this instance of Location.
151
157
  #
158
+ # @raise [AmbiguousCookbookName] if the CachedCookbook's name does not match the locations's name attribute
159
+ #
152
160
  # @return [Boolean]
153
161
  def validate_cached(cached_cookbook)
154
162
  unless version_constraint.satisfies?(cached_cookbook.version)
155
- raise ConstraintNotSatisfied, "A cookbook satisfying '#{name}' (#{version_constraint}) not found at #{self}"
163
+ raise ConstraintNotSatisfied, "A cookbook satisfying '#{self.name}' (#{self.version_constraint}) not found at #{self}"
156
164
  end
157
165
 
166
+ # JW TODO: Safe to uncomment when when Opscode makes the 'name' a required attribute in Cookbook metadata
167
+ # unless self.name == cached_cookbook.cookbook_name
168
+ # raise AmbiguousCookbookName, "Expected a cookbook at #{self} to be named '#{self.name}'. Did you set the 'name' attribute in your Cookbooks metadata? If you didn't, the name of the directory will be used as the name of your Cookbook (awful, right?)."
169
+ # end
170
+
158
171
  true
159
172
  end
160
173
 
174
+ def to_hash
175
+ {
176
+ type: self.class.location_key
177
+ }
178
+ end
179
+
180
+ def to_json
181
+ MultiJson.dump(self.to_hash, pretty: true)
182
+ end
183
+
161
184
  private
162
185
 
163
186
  def set_downloaded_status(state)
@@ -56,12 +56,26 @@ module Berkshelf
56
56
 
57
57
  true
58
58
  end
59
+
60
+ # Retrieves the organization of a Chef API URI. If the URI does not contain an
61
+ # organization then nil will be returned.
62
+ #
63
+ # @param [String] uri
64
+ #
65
+ # @raise [InvalidChefAPILocation]
66
+ #
67
+ # @return [String, nil]
68
+ def extract_organization(uri)
69
+ validate_uri!(uri)
70
+
71
+ URI(uri).path.split('organizations/')[1]
72
+ end
59
73
  end
60
74
 
61
75
  include Location
62
76
 
63
- location_key :chef_api
64
- valid_options :node_name, :client_key
77
+ set_location_key :chef_api
78
+ set_valid_options :node_name, :client_key
65
79
 
66
80
  attr_reader :uri
67
81
  attr_reader :node_name
@@ -82,9 +96,9 @@ module Berkshelf
82
96
  # the filepath to the authentication key for the client
83
97
  # Default: Chef::Config[:client_key]
84
98
  def initialize(name, version_constraint, options = {})
85
- @name = name
99
+ @name = name
86
100
  @version_constraint = version_constraint
87
- @downloaded_status = false
101
+ @downloaded_status = false
88
102
 
89
103
  validate_options!(options)
90
104
 
@@ -170,8 +184,12 @@ module Berkshelf
170
184
  [ version, versions[version] ]
171
185
  end
172
186
 
187
+ def to_hash
188
+ super.merge(value: self.uri)
189
+ end
190
+
173
191
  def to_s
174
- "chef_api: '#{uri}'"
192
+ "#{self.class.location_key}: '#{uri}'"
175
193
  end
176
194
 
177
195
  private
@@ -3,8 +3,8 @@ module Berkshelf
3
3
  class GitLocation
4
4
  include Location
5
5
 
6
- location_key :git
7
- valid_options :ref, :branch, :tag
6
+ set_location_key :git
7
+ set_valid_options :ref, :branch, :tag
8
8
 
9
9
  attr_accessor :uri
10
10
  attr_accessor :branch
@@ -25,10 +25,10 @@ module Berkshelf
25
25
  # @option options [String] :tag
26
26
  # same as tag
27
27
  def initialize(name, version_constraint, options = {})
28
- @name = name
28
+ @name = name
29
29
  @version_constraint = version_constraint
30
- @uri = options[:git]
31
- @branch = options[:branch] || options[:ref] || options[:tag]
30
+ @uri = options[:git]
31
+ @branch = options[:branch] || options[:ref] || options[:tag]
32
32
 
33
33
  Git.validate_uri!(@uri)
34
34
  end
@@ -61,8 +61,15 @@ module Berkshelf
61
61
  cached
62
62
  end
63
63
 
64
+ def to_hash
65
+ super.tap do |h|
66
+ h[:value] = self.uri
67
+ h[:branch] = self.branch if branch
68
+ end
69
+ end
70
+
64
71
  def to_s
65
- s = "git: '#{uri}'"
72
+ s = "#{self.class.location_key}: '#{uri}'"
66
73
  s << " with branch: '#{branch}'" if branch
67
74
  s
68
75
  end
@@ -3,7 +3,7 @@ module Berkshelf
3
3
  class PathLocation
4
4
  include Location
5
5
 
6
- location_key :path
6
+ set_location_key :path
7
7
 
8
8
  attr_accessor :path
9
9
 
@@ -14,9 +14,9 @@ module Berkshelf
14
14
  # @option options [String] :path
15
15
  # a filepath to the cookbook on your local disk
16
16
  def initialize(name, version_constraint, options = {})
17
- @name = name
17
+ @name = name
18
18
  @version_constraint = version_constraint
19
- @path = File.expand_path(options[:path])
19
+ @path = File.expand_path(options[:path])
20
20
  set_downloaded_status(true)
21
21
  end
22
22
 
@@ -31,8 +31,12 @@ module Berkshelf
31
31
  cached
32
32
  end
33
33
 
34
+ def to_hash
35
+ super.merge(value: self.path)
36
+ end
37
+
34
38
  def to_s
35
- "path: '#{path}'"
39
+ "#{self.class.location_key}: '#{path}'"
36
40
  end
37
41
  end
38
42
  end
@@ -1,9 +1,11 @@
1
+ require 'chef/rest'
2
+
1
3
  module Berkshelf
2
4
  # @author Jamie Winsor <jamie@vialstudios.com>
3
5
  class SiteLocation
4
6
  include Location
5
7
 
6
- location_key :site
8
+ set_location_key :site
7
9
 
8
10
  attr_reader :api_uri
9
11
  attr_accessor :version_constraint
@@ -26,7 +28,7 @@ module Berkshelf
26
28
  # a URL pointing to a community API endpoint. Alternatively the symbol :opscode can
27
29
  # be provided to initialize a SiteLocation pointing to the Opscode Community Site.
28
30
  def initialize(name, version_constraint, options = {})
29
- @name = name
31
+ @name = name
30
32
  @version_constraint = version_constraint
31
33
 
32
34
  @api_uri = if options[:site].nil? || options[:site] == :opscode
@@ -110,8 +112,12 @@ module Berkshelf
110
112
  end
111
113
  end
112
114
 
115
+ def to_hash
116
+ super.merge(value: self.api_uri)
117
+ end
118
+
113
119
  def to_s
114
- "site: '#{api_uri}'"
120
+ "#{self.class.location_key}: '#{api_uri}'"
115
121
  end
116
122
 
117
123
  private
@@ -0,0 +1,34 @@
1
+ module Berkshelf
2
+ class UI < Thor::Shell::Color
3
+ # Mute the output of this instance of UI until {#unmute!} is called
4
+ def mute!
5
+ @mute = true
6
+ end
7
+
8
+ # Unmute the output of this instance of UI until {#mute!} is called
9
+ def unmute!
10
+ @mute = false
11
+ end
12
+
13
+ def say(message, color = nil, force_new_line = (message.to_s !~ /( |\t)$/))
14
+ return if quiet?
15
+
16
+ super(message, color, force_new_line)
17
+ end
18
+ alias_method :info, :say
19
+
20
+ def say_status(status, message, log_status = true)
21
+ return if quiet?
22
+
23
+ super(status, message, log_status)
24
+ end
25
+
26
+ def error(message, color = :red)
27
+ return if quiet?
28
+
29
+ message = set_color(message, *color) if color
30
+ super(message)
31
+ end
32
+ alias_method :fatal, :error
33
+ end
34
+ end
@@ -1,32 +1,24 @@
1
- require 'rest_client'
2
- require 'chef/sandbox'
3
- require 'chef/config'
4
-
5
1
  module Berkshelf
6
2
  # @author Jamie Winsor <jamie@vialstudios.com>
7
3
  class Uploader
8
- attr_reader :server_url
9
- attr_reader :queue
4
+ extend Forwardable
10
5
 
11
- # @param [String] server_url
12
- # the URL to the Chef Server to upload Cookbooks to
13
- #
14
- # @option options [String] :node_name
15
- # the name of the client used to sign REST requests to the Chef Server
16
- #
17
- # Default: the value of Chef::Config[:node_name]
18
- # @option options [String] :client_key
19
- # the filepath location for the client's key used to sign REST requests
20
- # to the Chef Server
21
- #
22
- # Default: the value of Chef::Config[:client_key]
23
- def initialize(server_url, options = {})
24
- options[:node_name] ||= Chef::Config[:node_name]
25
- options[:client_key] ||= Chef::Config[:client_key]
6
+ def_delegator :conn, :client_name
7
+ def_delegator :conn, :client_key
8
+ def_delegator :conn, :organization
26
9
 
27
- @server_url = server_url
28
- @rest = Chef::REST.new(server_url, options[:node_name], options[:client_key])
29
- @queue = []
10
+ # @option options [String] :server_url
11
+ # URL to the Chef API
12
+ # @option options [String] :client_name
13
+ # name of the client used to authenticate with the Chef API
14
+ # @option options [String] :client_key
15
+ # filepath to the client's private key used to authenticate with
16
+ # the Chef API
17
+ # @option options [String] :organization
18
+ # the Organization to connect to. This is only used if you are connecting to
19
+ # private Chef or hosted Chef
20
+ def initialize(options = {})
21
+ @conn = Ridley.connection(options)
30
22
  end
31
23
 
32
24
  # Uploads a CachedCookbook from a CookbookStore to this instances Chef Server URL
@@ -41,93 +33,35 @@ module Berkshelf
41
33
  # Freeze the uploaded Cookbook on the Chef Server so that it cannot be
42
34
  # overwritten
43
35
  #
36
+ # @raise [CookbookNotFound]
37
+ # @raise [CookbookSyntaxError]
38
+ #
44
39
  # @return [Boolean]
45
40
  def upload(cookbook, options = {})
46
41
  cookbook.validate!
47
-
42
+ mutex = Mutex.new
48
43
  checksums = cookbook.checksums.dup
49
- new_sandbox = create_sandbox(checksums)
50
- upload_checksums_to_sandbox(checksums, new_sandbox)
51
- commit_sandbox(new_sandbox)
52
- save_cookbook(cookbook, options)
44
+ sandbox = conn.sandbox.create(checksums.keys)
53
45
 
54
- true
55
- end
56
-
57
- private
58
-
59
- attr_reader :rest
60
-
61
- def create_sandbox(checksums)
62
- massaged_sums = checksums.inject({}) do |memo, elt|
63
- memo[elt.first] = nil
64
- memo
65
- end
66
-
67
- rest.post_rest("sandboxes", checksums: massaged_sums)
68
- end
69
-
70
- def commit_sandbox(sandbox)
71
- # Retry if S3 is claims a checksum doesn't exist (the eventual
72
- # in eventual consistency)
73
- retries = 0
74
- begin
75
- rest.put_rest(sandbox['uri'], is_completed: true)
76
- rescue Net::HTTPServerException => e
77
- if e.message =~ /^400/ && (retries += 1) <= 5
78
- sleep 2
79
- retry
80
- else
81
- raise
82
- end
83
- end
84
- end
85
-
86
- def upload_checksums_to_sandbox(checksums, sandbox)
87
- sandbox['checksums'].each do |checksum, info|
88
- if info['needs_upload'] == true
89
- # JW TODO: threads, fibers, or evented uploads here
90
- upload_file(checksums[checksum], checksum, info['url'])
46
+ conn.thread_count.times.collect do
47
+ Thread.new(conn, sandbox, checksums.to_a) do |conn, sandbox, checksums|
48
+ while checksum = mutex.synchronize { checksums.pop }
49
+ sandbox.upload(checksum[0], checksum[1])
91
50
  end
92
51
  end
93
- end
94
-
95
- def upload_file(file, checksum, url)
96
- # Checksum is the hexadecimal representation of the md5, but we
97
- # need the base64 encoding for the content-md5 header
98
- checksum64 = Base64.encode64([checksum].pack("H*")).strip
99
- timestamp = Time.now.utc.iso8601
100
- file_contents = File.open(file, "rb") {|f| f.read}
101
-
102
- sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(
103
- http_method: :put,
104
- path: URI.parse(url).path,
105
- body: file_contents,
106
- timestamp: timestamp,
107
- user_id: rest.client_name
108
- )
109
- headers = { 'content-type' => 'application/x-binary', 'content-md5' => checksum64, accept: 'application/json' }
110
- headers.merge!(sign_obj.sign(OpenSSL::PKey::RSA.new(rest.signing_key)))
111
-
112
- begin
113
- RestClient::Resource.new(url, headers: headers, timeout: 1800, open_timeout: 1800).put(file_contents)
114
- rescue RestClient::Exception => e
115
- raise Berkshelf::UploadFailure, "Failed to upload 'file' to 'url': #{e.message}\n#{e.response.body}"
116
- end
117
- end
118
-
119
- def save_cookbook(cookbook, options = {})
120
- options[:freeze] ||= false
121
- options[:force] ||= false
122
-
123
- url = "cookbooks/#{cookbook.cookbook_name}/#{cookbook.version}"
124
- url << "?force=true" if options[:force]
52
+ end.each(&:join)
53
+
54
+ sandbox.commit
55
+ conn.cookbook.save(
56
+ cookbook.cookbook_name,
57
+ cookbook.version,
58
+ cookbook.to_json,
59
+ options
60
+ )
61
+ end
125
62
 
126
- rest.put_rest(url, cookbook)
127
- end
63
+ private
128
64
 
129
- def validate_source(source)
130
- source.is_a?(Berkshelf::CookbookSource)
131
- end
65
+ attr_reader :conn
132
66
  end
133
67
  end
@@ -0,0 +1,65 @@
1
+ require 'vagrant'
2
+ require 'berkshelf'
3
+
4
+ module Berkshelf
5
+ # @author Jamie Winsor <jamie@vialstudios.com>
6
+ # @author Andrew Garson <andrew.garson@gmail.com>
7
+ module Vagrant
8
+ module Action
9
+ autoload :Install, 'berkshelf/vagrant/action/install'
10
+ autoload :Upload, 'berkshelf/vagrant/action/upload'
11
+ autoload :Clean, 'berkshelf/vagrant/action/clean'
12
+ autoload :SetUI, 'berkshelf/vagrant/action/set_ui'
13
+ end
14
+
15
+ autoload :Config, 'berkshelf/vagrant/config'
16
+ autoload :Middleware, 'berkshelf/vagrant/middleware'
17
+
18
+ class << self
19
+ # @param [Vagrant::Action::Environment] env
20
+ def shelf_for(env)
21
+ File.join(Berkshelf.berkshelf_path, "vagrant", env[:global_config].vm.host_name)
22
+ end
23
+
24
+ # @param [Symbol] shortcut
25
+ # @param [Vagrant::Config::Top] config
26
+ #
27
+ # @return [Array]
28
+ def provisioners(shortcut, config)
29
+ config.vm.provisioners.select { |prov| prov.shortcut == shortcut }
30
+ end
31
+
32
+ # Determine if the given instance of Vagrant::Config::Top contains a
33
+ # chef_solo provisioner
34
+ #
35
+ # @param [Vagrant::Config::Top] config
36
+ #
37
+ # @return [Boolean]
38
+ def chef_solo?(config)
39
+ !provisioners(:chef_solo, config).empty?
40
+ end
41
+
42
+ # Determine if the given instance of Vagrant::Config::Top contains a
43
+ # chef_client provisioner
44
+ #
45
+ # @param [Vagrant::Config::Top] config
46
+ #
47
+ # @return [Boolean]
48
+ def chef_client?(config)
49
+ !provisioners(:chef_client, config).empty?
50
+ end
51
+
52
+ # Initialize the Berkshelf Vagrant middleware stack
53
+ def init!
54
+ ::Vagrant.config_keys.register(:berkshelf) { Berkshelf::Vagrant::Config }
55
+ ::Vagrant.actions[:provision].insert(::Vagrant::Action::VM::Provision, Berkshelf::Vagrant::Middleware.install)
56
+ ::Vagrant.actions[:provision].insert(::Vagrant::Action::VM::Provision, Berkshelf::Vagrant::Middleware.upload)
57
+ ::Vagrant.actions[:start].insert(::Vagrant::Action::VM::Provision, Berkshelf::Vagrant::Middleware.install)
58
+ ::Vagrant.actions[:start].insert(::Vagrant::Action::VM::Provision, Berkshelf::Vagrant::Middleware.upload)
59
+ ::Vagrant.actions[:destroy].insert(::Vagrant::Action::VM::CleanMachineFolder, Berkshelf::Vagrant::Middleware.clean)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ Berkshelf::Vagrant.init!