knife-essentials 0.9.2 → 0.9.3

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