knife-essentials 0.9.2 → 0.9.3

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.
@@ -1,4 +1,5 @@
1
1
  require 'json'
2
+ require 'chef_fs/data_handler/data_handler_base'
2
3
 
3
4
  class Chef
4
5
  class Knife
@@ -41,7 +42,7 @@ class Chef
41
42
  end
42
43
  chef_rest = Chef::REST.new(Chef::Config[:chef_server_url])
43
44
  begin
44
- output api_request(chef_rest, config[:method].to_sym, chef_rest.create_url(name_args[0]), {}, data)
45
+ output ChefFS::FileSystem::BaseFSObject.api_request(chef_rest, config[:method].to_sym, chef_rest.create_url(name_args[0]), {}, data)
45
46
  rescue Net::HTTPServerException => e
46
47
  ui.error "Server responded with error #{e.response.code} \"#{e.response.message}\""
47
48
  ui.error "Error Body: #{e.response.body}" if e.response.body && e.response.body != ''
@@ -49,66 +50,7 @@ class Chef
49
50
  end
50
51
  end
51
52
 
52
- ACCEPT_ENCODING = "Accept-Encoding".freeze
53
- ENCODING_GZIP_DEFLATE = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3".freeze
54
-
55
- def redirected_to(response)
56
- return nil unless response.kind_of?(Net::HTTPRedirection)
57
- # Net::HTTPNotModified is undesired subclass of Net::HTTPRedirection so test for this
58
- return nil if response.kind_of?(Net::HTTPNotModified)
59
- response['location']
60
- end
61
-
62
- def api_request(chef_rest, method, url, headers={}, data=false)
63
- json_body = data
64
- # json_body = data ? Chef::JSONCompat.to_json(data) : nil
65
- # Force encoding to binary to fix SSL related EOFErrors
66
- # cf. http://tickets.opscode.com/browse/CHEF-2363
67
- # http://redmine.ruby-lang.org/issues/5233
68
- # json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding)
69
- headers = build_headers(chef_rest, method, url, headers, json_body)
70
-
71
- chef_rest.retriable_rest_request(method, url, json_body, headers) do |rest_request|
72
- response = rest_request.call {|r| r.read_body}
73
-
74
- response_body = chef_rest.decompress_body(response)
75
-
76
- if response.kind_of?(Net::HTTPSuccess)
77
- if config[:pretty] && response['content-type'] =~ /json/
78
- JSON.pretty_generate(JSON.parse(response_body, :create_additions => false))
79
- else
80
- response_body
81
- end
82
- elsif redirect_location = redirected_to(response)
83
- raise "Redirected to #{create_url(redirect_location)}"
84
- follow_redirect {api_request(:GET, create_url(redirect_location))}
85
- else
86
- # have to decompress the body before making an exception for it. But the body could be nil.
87
- response.body.replace(chef_rest.decompress_body(response)) if response.body.respond_to?(:replace)
88
-
89
- if response['content-type'] =~ /json/
90
- exception = response_body
91
- msg = "HTTP Request Returned #{response.code} #{response.message}: "
92
- msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s)
93
- Chef::Log.info(msg)
94
- end
95
- response.error!
96
- end
97
- end
98
- end
99
-
100
- def build_headers(chef_rest, method, url, headers={}, json_body=false, raw=false)
101
- # headers = @default_headers.merge(headers)
102
- #headers['Accept'] = "application/json" unless raw
103
- headers['Accept'] = "application/json" unless raw
104
- headers["Content-Type"] = 'application/json' if json_body
105
- headers['Content-Length'] = json_body.bytesize.to_s if json_body
106
- headers[Chef::REST::RESTRequest::ACCEPT_ENCODING] = Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE
107
- headers.merge!(chef_rest.authentication_headers(method, url, json_body)) if chef_rest.sign_requests?
108
- headers.merge!(Chef::Config[:custom_http_headers]) if Chef::Config[:custom_http_headers]
109
- headers
110
- end
111
- end
53
+ end # class Raw
112
54
  end
113
55
  end
114
56
 
@@ -105,6 +105,7 @@ module ChefFS
105
105
  end
106
106
  result
107
107
  end
108
- end
108
+
109
+ end # class DataHandlerBase
109
110
  end
110
111
  end
@@ -103,6 +103,11 @@ module ChefFS
103
103
  []
104
104
  end
105
105
 
106
+ def chef_hash
107
+ raise NotFoundError.new(self) if !exists?
108
+ nil
109
+ end
110
+
106
111
  # Expand this entry into a chef object (Chef::Role, ::Node, etc.)
107
112
  def chef_object
108
113
  raise NotFoundError.new(self) if !exists?
@@ -171,7 +176,76 @@ module ChefFS
171
176
  # Important directory attributes: name, parent, path, root
172
177
  # Overridable attributes: dir?, child(name), path_for_printing
173
178
  # Abstract: read, write, delete, children, can_have_child?, create_child, compare_to
174
- end
179
+
180
+ # Consider putting this into a concern module and including it instead
181
+ def raw_request(_api_path)
182
+ self.class.api_request(rest, :GET, rest.create_url(_api_path), {}, false)
183
+ end
184
+
185
+
186
+ class << self
187
+ # Copied so that it does not automatically inflate an object
188
+ # This is also used by knife raw_essentials
189
+
190
+ ACCEPT_ENCODING = "Accept-Encoding".freeze
191
+ ENCODING_GZIP_DEFLATE = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3".freeze
192
+
193
+ def redirected_to(response)
194
+ return nil unless response.kind_of?(Net::HTTPRedirection)
195
+ # Net::HTTPNotModified is undesired subclass of Net::HTTPRedirection so test for this
196
+ return nil if response.kind_of?(Net::HTTPNotModified)
197
+ response['location']
198
+ end
199
+
200
+
201
+ def build_headers(chef_rest, method, url, headers={}, json_body=false, raw=false)
202
+ # headers = @default_headers.merge(headers)
203
+ #headers['Accept'] = "application/json" unless raw
204
+ headers['Accept'] = "application/json" unless raw
205
+ headers["Content-Type"] = 'application/json' if json_body
206
+ headers['Content-Length'] = json_body.bytesize.to_s if json_body
207
+ headers[Chef::REST::RESTRequest::ACCEPT_ENCODING] = Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE
208
+ headers.merge!(chef_rest.authentication_headers(method, url, json_body)) if chef_rest.sign_requests?
209
+ headers.merge!(Chef::Config[:custom_http_headers]) if Chef::Config[:custom_http_headers]
210
+ headers
211
+ end
212
+
213
+ def api_request(chef_rest, method, url, headers={}, data=false)
214
+ json_body = data
215
+ # json_body = data ? Chef::JSONCompat.to_json(data) : nil
216
+ # Force encoding to binary to fix SSL related EOFErrors
217
+ # cf. http://tickets.opscode.com/browse/CHEF-2363
218
+ # http://redmine.ruby-lang.org/issues/5233
219
+ # json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding)
220
+ headers = build_headers(chef_rest, method, url, headers, json_body)
221
+
222
+ chef_rest.retriable_rest_request(method, url, json_body, headers) do |rest_request|
223
+ response = rest_request.call {|r| r.read_body}
224
+
225
+ response_body = chef_rest.decompress_body(response)
226
+
227
+ if response.kind_of?(Net::HTTPSuccess)
228
+ response_body
229
+ elsif redirect_location = redirected_to(response)
230
+ raise "Redirected to #{create_url(redirect_location)}"
231
+ follow_redirect {api_request(:GET, create_url(redirect_location))}
232
+ else
233
+ # have to decompress the body before making an exception for it. But the body could be nil.
234
+ response.body.replace(chef_rest.decompress_body(response)) if response.body.respond_to?(:replace)
235
+
236
+ if response['content-type'] =~ /json/
237
+ exception = response_body
238
+ msg = "HTTP Request Returned #{response.code} #{response.message}: "
239
+ msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s)
240
+ Chef::Log.info(msg)
241
+ end
242
+ response.error!
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ end # class BaseFsObject
175
249
  end
176
250
  end
177
251
 
@@ -1,5 +1,6 @@
1
1
  #
2
2
  # Author:: John Keiser (<jkeiser@opscode.com>)
3
+ # Author:: Ho-Sheng Hsiao (<hosh@opscode.com>)
3
4
  # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
5
  # License:: Apache License, Version 2.0
5
6
  #
@@ -45,6 +46,17 @@ module ChefFS
45
46
  begin
46
47
  if parent.path == '/cookbooks'
47
48
  loader = Chef::Cookbook::CookbookVersionLoader.new(file_path, parent.chefignore)
49
+ # We need the canonical cookbook name if we are using versioned cookbooks, but we don't
50
+ # want to spend a lot of time adding code to the main Chef libraries
51
+ if Chef::Config[:versioned_cookbooks]
52
+
53
+ _canonical_name = canonical_cookbook_name(File.basename(file_path))
54
+ fail "When versioned_cookbooks mode is on, cookbook #{file_path} must match format <cookbook_name>-x.y.z" unless _canonical_name
55
+
56
+ # KLUDGE: We shouldn't have to use instance_variable_set
57
+ loader.instance_variable_set(:@cookbook_name, _canonical_name)
58
+ end
59
+
48
60
  loader.load_cookbooks
49
61
  return loader.cookbook_version
50
62
  end
@@ -57,6 +69,17 @@ module ChefFS
57
69
  nil
58
70
  end
59
71
 
72
+ # Exposed as a class method so that it can be used elsewhere
73
+ def self.canonical_cookbook_name(entry_name)
74
+ name_match = ChefFS::FileSystem::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME.match(entry_name)
75
+ return nil if name_match.nil?
76
+ return name_match[1]
77
+ end
78
+
79
+ def canonical_cookbook_name(entry_name)
80
+ self.class.canonical_cookbook_name(entry_name)
81
+ end
82
+
60
83
  def children
61
84
  @children ||=
62
85
  Dir.entries(file_path).
@@ -26,12 +26,24 @@ require 'chef/cookbook_uploader'
26
26
  module ChefFS
27
27
  module FileSystem
28
28
  class CookbookDir < BaseFSDir
29
- def initialize(name, parent, versions = nil)
29
+ def initialize(name, parent, options = {})
30
30
  super(name, parent)
31
- @versions = versions
31
+ @exists = options[:exists]
32
+ # If the name is apache2-1.0.0 and versioned_cookbooks is on, we know
33
+ # the actual cookbook_name and version.
34
+ if Chef::Config[:versioned_cookbooks]
35
+ if name =~ VALID_VERSIONED_COOKBOOK_NAME
36
+ @cookbook_name = $1
37
+ @version = $2
38
+ else
39
+ @exists = false
40
+ end
41
+ else
42
+ @cookbook_name = name
43
+ end
32
44
  end
33
45
 
34
- attr_reader :versions
46
+ attr_reader :cookbook_name, :version
35
47
 
36
48
  COOKBOOK_SEGMENT_INFO = {
37
49
  :attributes => { :ruby_only => true },
@@ -45,12 +57,16 @@ module ChefFS
45
57
  :root_files => { }
46
58
  }
47
59
 
60
+ # See Erchef code
61
+ # https://github.com/opscode/chef_objects/blob/968a63344d38fd507f6ace05f73d53e9cd7fb043/src/chef_regex.erl#L94
62
+ VALID_VERSIONED_COOKBOOK_NAME = /^([.a-zA-Z0-9_-]+)-(\d+\.\d+\.\d+)$/
63
+
48
64
  def add_child(child)
49
65
  @children << child
50
66
  end
51
67
 
52
68
  def api_path
53
- "#{parent.api_path}/#{name}/_latest"
69
+ "#{parent.api_path}/#{cookbook_name}/#{version || "_latest"}"
54
70
  end
55
71
 
56
72
  def child(name)
@@ -67,11 +83,8 @@ module ChefFS
67
83
 
68
84
  def can_have_child?(name, is_dir)
69
85
  # A cookbook's root may not have directories unless they are segment directories
70
- if is_dir
71
- return name != 'root_files' &&
72
- COOKBOOK_SEGMENT_INFO.keys.any? { |segment| segment.to_s == name }
73
- end
74
- true
86
+ return name != 'root_files' && COOKBOOK_SEGMENT_INFO.keys.include?(name.to_sym) if is_dir
87
+ return true
75
88
  end
76
89
 
77
90
  def children
@@ -123,12 +136,13 @@ module ChefFS
123
136
  end
124
137
  end
125
138
 
139
+ # In versioned cookbook mode, actually check if the version exists
140
+ # Probably want to cache this.
126
141
  def exists?
127
- if !@versions
128
- child = parent.children.select { |child| child.name == name }.first
129
- @versions = child.versions if child
142
+ if @exists.nil?
143
+ @exists = parent.children.any? { |child| child.name == name }
130
144
  end
131
- !!@versions
145
+ @exists
132
146
  end
133
147
 
134
148
  def compare_to(other)
@@ -19,6 +19,8 @@
19
19
  require 'chef_fs/file_system/rest_list_dir'
20
20
  require 'chef_fs/file_system/cookbook_dir'
21
21
 
22
+ require 'tmpdir'
23
+
22
24
  module ChefFS
23
25
  module FileSystem
24
26
  class CookbooksDir < RestListDir
@@ -27,12 +29,32 @@ module ChefFS
27
29
  end
28
30
 
29
31
  def child(name)
30
- result = @children.select { |child| child.name == name }.first if @children
31
- result || CookbookDir.new(name, self)
32
+ if @children
33
+ result = self.children.select { |child| child.name == name }.first
34
+ if result
35
+ result
36
+ else
37
+ NonexistentFSObject.new(name, self)
38
+ end
39
+ else
40
+ CookbookDir.new(name, self)
41
+ end
32
42
  end
33
43
 
34
44
  def children
35
- @children ||= rest.get_rest(api_path).map { |key, value| CookbookDir.new(key, self, value) }.sort_by { |c| c.name }
45
+ @children ||= begin
46
+ if Chef::Config[:versioned_cookbooks]
47
+ result = []
48
+ rest.get_rest("#{api_path}/?num_versions=all").each_pair do |cookbook_name, cookbooks|
49
+ cookbooks['versions'].each do |cookbook_version|
50
+ result << CookbookDir.new("#{cookbook_name}-#{cookbook_version['version']}", self, :exists => true)
51
+ end
52
+ end
53
+ else
54
+ result = rest.get_rest(api_path).keys.map { |cookbook_name| CookbookDir.new(cookbook_name, self, :exists => true) }
55
+ end
56
+ result.sort_by(&:name)
57
+ end
36
58
  end
37
59
 
38
60
  def create_child_from(other)
@@ -40,38 +62,75 @@ module ChefFS
40
62
  end
41
63
 
42
64
  def upload_cookbook_from(other)
43
- other_cookbook_version = other.chef_object
44
- # TODO this only works on the file system. And it can't be broken into
45
- # pieces.
46
- begin
47
- uploader = Chef::CookbookUploader.new(other_cookbook_version, other.parent.file_path, :rest => rest)
48
- # Work around the fact that CookbookUploader doesn't understand chef_repo_path (yet)
49
- old_cookbook_path = Chef::Config.cookbook_path
50
- Chef::Config.cookbook_path = other.parent.file_path if !Chef::Config.cookbook_path
51
- begin
52
- # Chef 11 changes this API
53
- if uploader.respond_to?(:upload_cookbook)
54
- uploader.upload_cookbook
55
- else
56
- uploader.upload_cookbooks
57
- end
58
- ensure
59
- Chef::Config.cookbook_path = old_cookbook_path
60
- end
61
- rescue Net::HTTPServerException => e
62
- case e.response.code
63
- when "409"
64
- ui.error "Version #{other_cookbook_version.version} of cookbook #{other_cookbook_version.name} is frozen. Use --force to override."
65
- Chef::Log.debug(e)
66
- raise Exceptions::CookbookFrozen
67
- else
68
- raise
65
+ Chef::Config[:versioned_cookbooks] ? upload_versioned_cookbook(other) : upload_unversioned_cookbook(other)
66
+ rescue Net::HTTPServerException => e
67
+ case e.response.code
68
+ when "409"
69
+ ui.error "Version #{other_cookbook_version.version} of cookbook #{other_cookbook_version.name} is frozen. Use --force to override."
70
+ Chef::Log.debug(e)
71
+ raise Exceptions::CookbookFrozen
72
+ else
73
+ raise
74
+ end
75
+ end
76
+
77
+ # Knife currently does not understand versioned cookbooks
78
+ # Cookbook Version uploader also requires a lot of refactoring
79
+ # to make this work. So instead, we make a temporary cookbook
80
+ # symlinking back to real cookbook, and upload the proxy.
81
+ def upload_versioned_cookbook(other)
82
+ cookbook_name = ChefFS::FileSystem::ChefRepositoryFileSystemEntry.canonical_cookbook_name(other.name)
83
+
84
+ Dir.mktmpdir do |temp_cookbooks_path|
85
+ proxy_cookbook_path = "#{temp_cookbooks_path}/#{cookbook_name}"
86
+
87
+ # Make a symlink
88
+ File.symlink other.file_path, proxy_cookbook_path
89
+
90
+ # Instantiate a proxy loader using the temporary symlink
91
+ proxy_loader = Chef::Cookbook::CookbookVersionLoader.new(proxy_cookbook_path, other.parent.chefignore)
92
+ proxy_loader.load_cookbooks
93
+
94
+ # Instantiate a new uploader based on the proxy loader
95
+ uploader = Chef::CookbookUploader.new(proxy_loader.cookbook_version, proxy_cookbook_path, :rest => rest)
96
+
97
+ with_actual_cookbooks_dir(temp_cookbooks_path) do
98
+ upload_cookbook!(uploader)
69
99
  end
70
100
  end
71
101
  end
72
102
 
103
+ def upload_unversioned_cookbook(other)
104
+ uploader = Chef::CookbookUploader.new(other.chef_object, other.parent.file_path, :rest => rest)
105
+
106
+ with_actual_cookbooks_dir(other.parent.file_path) do
107
+ upload_cookbook!(uploader)
108
+ end
109
+ end
110
+
111
+ # Work around the fact that CookbookUploader doesn't understand chef_repo_path (yet)
112
+ def with_actual_cookbooks_dir(actual_cookbook_path)
113
+ old_cookbook_path = Chef::Config.cookbook_path
114
+ Chef::Config.cookbook_path = actual_cookbook_path if !Chef::Config.cookbook_path
115
+
116
+ yield
117
+ ensure
118
+ Chef::Config.cookbook_path = old_cookbook_path
119
+ end
120
+
121
+ # Chef 11 changes this API
122
+ def upload_cookbook!(uploader)
123
+ if uploader.respond_to?(:upload_cookbook)
124
+ uploader.upload_cookbook
125
+ else
126
+ uploader.upload_cookbooks
127
+ end
128
+ end
129
+
73
130
  def can_have_child?(name, is_dir)
74
- is_dir
131
+ return false if !is_dir
132
+ return false if Chef::Config[:versioned_cookbooks] && name !~ ChefFS::FileSystem::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME
133
+ return true
75
134
  end
76
135
  end
77
136
  end
@@ -33,7 +33,7 @@ module ChefFS
33
33
 
34
34
  def children
35
35
  begin
36
- @children ||= rest.get_rest(api_path).keys.sort.map do |entry|
36
+ @children ||= chef_collection.keys.sort.map do |entry|
37
37
  DataBagDir.new(entry, self, true)
38
38
  end
39
39
  rescue Net::HTTPServerException
@@ -45,6 +45,10 @@ module ChefFS
45
45
  end
46
46
  end
47
47
 
48
+ def chef_collection
49
+ rest.get_rest(api_path)
50
+ end
51
+
48
52
  def can_have_child?(name, is_dir)
49
53
  is_dir
50
54
  end