cfoundry 0.4.21 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Rakefile +47 -24
  2. data/lib/cfoundry/auth_token.rb +48 -0
  3. data/lib/cfoundry/baseclient.rb +96 -277
  4. data/lib/cfoundry/client.rb +2 -0
  5. data/lib/cfoundry/concerns/login_helpers.rb +13 -0
  6. data/lib/cfoundry/errors.rb +21 -13
  7. data/lib/cfoundry/rest_client.rb +290 -0
  8. data/lib/cfoundry/test_support.rb +3 -0
  9. data/lib/cfoundry/trace_helpers.rb +9 -9
  10. data/lib/cfoundry/uaaclient.rb +66 -74
  11. data/lib/cfoundry/upload_helpers.rb +2 -0
  12. data/lib/cfoundry/v1/app.rb +2 -2
  13. data/lib/cfoundry/v1/base.rb +4 -51
  14. data/lib/cfoundry/v1/client.rb +7 -30
  15. data/lib/cfoundry/v1/model.rb +22 -5
  16. data/lib/cfoundry/v1/model_magic.rb +30 -15
  17. data/lib/cfoundry/v2/app.rb +2 -5
  18. data/lib/cfoundry/v2/base.rb +10 -74
  19. data/lib/cfoundry/v2/client.rb +19 -30
  20. data/lib/cfoundry/v2/domain.rb +1 -4
  21. data/lib/cfoundry/v2/model.rb +1 -3
  22. data/lib/cfoundry/v2/model_magic.rb +13 -23
  23. data/lib/cfoundry/version.rb +1 -1
  24. data/lib/cfoundry/zip.rb +1 -1
  25. data/spec/cfoundry/auth_token_spec.rb +77 -0
  26. data/spec/cfoundry/baseclient_spec.rb +54 -30
  27. data/spec/cfoundry/errors_spec.rb +10 -13
  28. data/spec/cfoundry/rest_client_spec.rb +238 -0
  29. data/spec/cfoundry/trace_helpers_spec.rb +10 -5
  30. data/spec/cfoundry/uaaclient_spec.rb +141 -114
  31. data/spec/cfoundry/upload_helpers_spec.rb +129 -0
  32. data/spec/cfoundry/v1/base_spec.rb +2 -2
  33. data/spec/cfoundry/v1/client_spec.rb +17 -0
  34. data/spec/cfoundry/v1/model_magic_spec.rb +43 -0
  35. data/spec/cfoundry/v2/base_spec.rb +256 -33
  36. data/spec/cfoundry/v2/client_spec.rb +68 -0
  37. data/spec/cfoundry/v2/model_magic_spec.rb +49 -0
  38. data/spec/fixtures/apps/with_vmcignore/ignored_dir/file_in_ignored_dir.txt +1 -0
  39. data/spec/fixtures/apps/with_vmcignore/ignored_file.txt +1 -0
  40. data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/file_in_non_ignored_dir.txt +1 -0
  41. data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/ignored_file.txt +1 -0
  42. data/spec/fixtures/apps/with_vmcignore/non_ignored_file.txt +1 -0
  43. data/spec/fixtures/empty_file +0 -0
  44. data/spec/spec_helper.rb +4 -4
  45. data/spec/support/randoms.rb +3 -0
  46. data/spec/support/shared_examples/client_login_examples.rb +46 -0
  47. data/spec/support/{summaries.rb → shared_examples/model_summary_examples.rb} +0 -0
  48. data/spec/support/v1_fake_helper.rb +144 -0
  49. metadata +101 -37
  50. data/lib/cfoundry/spec_helper.rb +0 -1
data/Rakefile CHANGED
@@ -1,41 +1,64 @@
1
1
  require "rake"
2
+ require "rspec/core/rake_task"
2
3
 
3
4
  $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
4
5
  require "cfoundry/version"
5
6
 
6
- task :default => "spec"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ task :default => :spec
7
9
 
8
- desc "Run specs"
9
- task "spec" => ["bundler:install", "test:spec"]
10
+ namespace :deploy do
11
+ def last_staging_sha
12
+ `git rev-parse latest-staging`.strip
13
+ end
10
14
 
11
- task :build do
12
- sh "gem build cfoundry.gemspec"
13
- end
15
+ def last_release_sha
16
+ `git rev-parse latest-release`.strip
17
+ end
14
18
 
15
- task :install => :build do
16
- sh "gem install --local cfoundry-#{CFoundry::VERSION}"
17
- sh "rm cfoundry-#{CFoundry::VERSION}.gem"
18
- end
19
+ def last_staging_ref_was_released?
20
+ last_staging_sha == last_release_sha
21
+ end
19
22
 
20
- task :uninstall do
21
- sh "gem uninstall cfoundry"
23
+ task :staging, :version do |_, args|
24
+ sh "gem bump --push #{"--version #{args.version}" if args.version}" if last_staging_ref_was_released?
25
+ sh "git tag -f latest-staging"
26
+ sh "git push origin :latest-staging"
27
+ sh "git push origin latest-staging"
28
+ end
29
+
30
+ task :gem do
31
+ sh "git fetch"
32
+ sh "git checkout #{last_staging_sha}"
33
+ sh "gem release --tag"
34
+ sh "git tag -f latest-release"
35
+ sh "git push origin :latest-release"
36
+ sh "git push origin latest-release"
37
+ end
22
38
  end
23
39
 
24
- task :reinstall => [:uninstall, :install]
40
+ namespace :release do
41
+ DEPENDENTS = %w[
42
+ vmc/vmc.gemspec
43
+ vmc-plugins/admin/admin-vmc-plugin.gemspec
44
+ vmc-plugins/console/console-vmc-plugin.gemspec
45
+ vmc-plugins/manifests/manifests-vmc-plugin.gemspec
46
+ vmc-plugins/mcf/mcf-vmc-plugin.gemspec
47
+ vmc-plugins/tunnel/tunnel-vmc-plugin.gemspec
48
+ ].freeze
25
49
 
26
- task :release => :build do
27
- sh "gem push cfoundry-#{CFoundry::VERSION}.gem"
28
- end
50
+ def bump_dependent(file, dep, ver)
51
+ puts "Bumping #{dep} to #{ver} in #{file}"
29
52
 
30
- namespace "bundler" do
31
- desc "Install gems"
32
- task "install" do
33
- sh("bundle install")
53
+ old = File.read(file)
54
+ new = old.sub(/(\.add.+#{dep}\D+)[^'"]+(.+)/, "\\1#{ver}\\2")
55
+
56
+ File.open(file, "w") { |io| io.print new }
34
57
  end
35
- end
36
58
 
37
- namespace "test" do
38
- task "spec" do |t|
39
- sh("bundle exec rspec")
59
+ task :bump_dependents do
60
+ DEPENDENTS.each do |dep|
61
+ bump_dependent(File.expand_path("../../#{dep}", __FILE__), "cfoundry", CFoundry::VERSION)
62
+ end
40
63
  end
41
64
  end
@@ -0,0 +1,48 @@
1
+ module CFoundry
2
+ class AuthToken
3
+ class << self
4
+ def from_uaa_token_info(token_info)
5
+ new(
6
+ token_info.auth_header,
7
+ token_info.info[:refresh_token]
8
+ )
9
+ end
10
+
11
+ def from_hash(hash)
12
+ new(
13
+ hash[:token],
14
+ hash[:refresh_token]
15
+ )
16
+ end
17
+ end
18
+
19
+ def initialize(auth_header, refresh_token = nil)
20
+ @auth_header = auth_header
21
+ @refresh_token = refresh_token
22
+ end
23
+
24
+ attr_reader :auth_header
25
+
26
+ def to_hash
27
+ {
28
+ :token => auth_header,
29
+ :refresh_token => @refresh_token
30
+ }
31
+ end
32
+
33
+ JSON_HASH = /\{.*?\}/.freeze
34
+
35
+ # TODO: rename to #data
36
+ def token_data
37
+ return @token_data if @token_data
38
+
39
+ json_hashes = Base64.decode64(@auth_header.split(" ", 2).last)
40
+ data_json = json_hashes.sub(JSON_HASH, "")[JSON_HASH]
41
+ return {} unless data_json
42
+
43
+ @token_data = MultiJson.load data_json, :symbolize_keys => true
44
+ rescue MultiJson::DecodeError
45
+ {}
46
+ end
47
+ end
48
+ end
@@ -3,335 +3,154 @@ require "net/https"
3
3
  require "net/http/post/multipart"
4
4
  require "multi_json"
5
5
  require "fileutils"
6
- require "base64"
6
+ require "forwardable"
7
7
 
8
8
  module CFoundry
9
9
  class BaseClient # :nodoc:
10
- include CFoundry::TraceHelpers
10
+ extend Forwardable
11
11
 
12
- LOG_LENGTH = 10
12
+ attr_reader :rest_client
13
13
 
14
- attr_reader :target
14
+ def_delegators :rest_client, :target, :target=, :token,
15
+ :proxy, :proxy=, :trace, :backtrace, :backtrace=,
16
+ :log, :log=
15
17
 
16
- attr_accessor :trace, :backtrace, :log
17
-
18
- def initialize(target, token = nil)
19
- @target = target
20
- @token = token
21
- @trace = false
22
- @backtrace = false
23
- @log = false
24
- end
25
-
26
- # grab the metadata from a token that looks like:
27
- #
28
- # bearer (base64 ...)
29
- def token_data
30
- return {} unless @token
31
-
32
- tok = Base64.decode64(@token.sub(/^bearer\s+/, ""))
33
- tok.sub!(/\{.+?\}/, "") # clear algo
34
- MultiJson.load(tok[/\{.+?\}/], :symbolize_keys => true)
35
-
36
- # can't expect all tokens to be the proper format
37
- rescue MultiJson::DecodeError
38
- {}
39
- end
40
-
41
- def request_path(method, path, options = {})
42
- path = url(path) if path.is_a?(Array)
43
-
44
- request(method, path, options)
45
- end
46
-
47
- def request(method, path, options = {})
48
- request_uri(URI.parse(@target + path), method, options)
18
+ def initialize(target = "https://api.cloudfoundry.com", token = nil)
19
+ @rest_client = CFoundry::RestClient.new(target, token)
20
+ self.trace = false
21
+ self.backtrace = false
22
+ self.log = false
49
23
  end
50
24
 
51
- def request_uri(uri, method, options = {})
52
- uri = URI.parse(@target + uri.to_s) unless uri.host
53
-
54
- # keep original options in case there's a redirect to follow
55
- original_options = options.dup
56
-
57
- accept = options.delete(:accept)
58
- content = options.delete(:content)
59
- payload = options.delete(:payload)
60
- params = options.delete(:params)
61
- return_headers = options.delete(:return_headers)
62
- return_response = options.delete(:return_response)
63
-
64
- headers = {}
65
- headers["Authorization"] = @token if @token
66
- headers["Proxy-User"] = @proxy if @proxy
67
-
68
- if accept_type = mimetype(accept)
69
- headers["Accept"] = accept_type
70
- end
71
-
72
- if content_type = mimetype(content)
73
- headers["Content-Type"] = content_type
74
- end
75
-
76
- unless payload.is_a?(String)
77
- case content
78
- when :json
79
- payload = MultiJson.dump(payload)
80
- when :form
81
- payload = encode_params(payload)
82
- end
83
- end
84
-
85
- if payload.is_a?(String)
86
- headers["Content-Length"] = payload.size
87
- elsif !payload
88
- headers["Content-Length"] = 0
25
+ def uaa
26
+ @uaa ||= begin
27
+ endpoint = info[:authorization_endpoint]
28
+ uaa = CFoundry::UAAClient.new(endpoint)
29
+ uaa.trace = trace
30
+ uaa.token = token
31
+ uaa
89
32
  end
33
+ end
90
34
 
91
- headers.merge!(options[:headers]) if options[:headers]
92
-
93
- if params
94
- if uri.query
95
- uri.query += "&" + encode_params(params)
96
- else
97
- uri.query = encode_params(params)
98
- end
99
- end
100
-
101
- if payload && payload.is_a?(Hash)
102
- multipart = method.const_get(:Multipart)
103
- request = multipart.new(uri.request_uri, payload)
104
- else
105
- request = method.new(uri.request_uri)
106
- request.body = payload if payload
107
- end
108
-
109
- request["Authorization"] = @token if @token
110
- request["Proxy-User"] = @proxy if @proxy
111
-
112
- headers.each do |k, v|
113
- request[k] = v
114
- end
115
-
116
- # TODO: test http proxies
117
- http = Net::HTTP.new(uri.host, uri.port)
118
-
119
- if uri.is_a?(URI::HTTPS)
120
- http.use_ssl = true
121
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
35
+ def token=(token)
36
+ if token.is_a?(String)
37
+ token = CFoundry::AuthToken.new(token)
122
38
  end
123
39
 
124
- print_request(request) if @trace
125
-
126
- before = Time.now
127
- http.start do
128
- response = http.request(request)
129
- time = Time.now - before
130
-
131
- print_response(response) if @trace
132
- print_backtrace(caller) if @trace
133
-
134
- log_request(time, request, response)
135
-
136
- if return_headers
137
- sane_headers(response)
138
- elsif return_response
139
- response
140
- elsif response.is_a?(Net::HTTPRedirection)
141
- request_uri(
142
- URI.parse(response["location"]),
143
- Net::HTTP::Get,
144
- original_options)
145
- else
146
- handle_response(response, accept, request)
147
- end
148
- end
149
- rescue ::Timeout::Error => e
150
- raise Timeout.new(method, uri, e)
151
- rescue SocketError, Errno::ECONNREFUSED => e
152
- raise TargetRefused, e.message
40
+ @rest_client.token = token
41
+ @uaa.token = token if @uaa
153
42
  end
154
43
 
155
- private
156
-
157
- def parse_json(x)
158
- if x.empty?
159
- raise MultiJson::DecodeError.new("Empty JSON string", [], "")
160
- else
161
- MultiJson.load(x, :symbolize_keys => true)
162
- end
44
+ def trace=(trace)
45
+ @rest_client.trace = trace
46
+ @uaa.trace = trace if @uaa
163
47
  end
164
48
 
165
- def mimetype(content)
166
- case content
167
- when String
168
- content
169
- when :json
170
- "application/json"
171
- when :form
172
- "application/x-www-form-urlencoded"
173
- when nil
174
- nil
175
- # return request headers (not really Accept)
176
- else
177
- raise CFoundry::Error, "Unknown mimetype '#{content.inspect}'"
178
- end
49
+ # Cloud metadata
50
+ def info
51
+ get("info", :accept => :json)
179
52
  end
180
53
 
181
- def encode_params(hash, escape = true)
182
- hash.keys.map do |k|
183
- v = hash[k]
184
- v = MultiJson.dump(v) if v.is_a?(Hash)
185
- v = URI.escape(v.to_s, /[^#{URI::PATTERN::UNRESERVED}]/) if escape
186
- "#{k}=#{v}"
187
- end.join("&")
54
+ def get(*args)
55
+ request("GET", *args)
188
56
  end
189
57
 
190
- def request_with_options(method, path, options = {})
191
- options.merge!(path.pop) if path.last.is_a?(Hash)
192
-
193
- request_path(method, url(path), options)
58
+ def delete(*args)
59
+ request("DELETE", *args)
194
60
  end
195
61
 
196
- def get(*path)
197
- request_with_options(Net::HTTP::Get, path)
62
+ def post(*args)
63
+ request("POST", *args)
198
64
  end
199
65
 
200
- def delete(*path)
201
- request_with_options(Net::HTTP::Delete, path)
66
+ def put(*args)
67
+ request("PUT", *args)
202
68
  end
203
69
 
204
- def post(payload, *path)
205
- request_with_options(Net::HTTP::Post, path, :payload => payload)
70
+ def request(method, *args)
71
+ path, options = normalize_arguments(args)
72
+ request, response = request_raw(method, path, options)
73
+ handle_response(response, options, request)
206
74
  end
207
75
 
208
- def put(payload, *path)
209
- request_with_options(Net::HTTP::Put, path, :payload => payload)
76
+ def request_raw(method, path, options)
77
+ @rest_client.request(method, path, options)
210
78
  end
211
79
 
212
- def url(segments)
213
- "/#{safe_path(segments)}"
214
- end
80
+ private
215
81
 
216
- def safe_path(*segments)
217
- segments.flatten.collect { |x|
218
- URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
219
- }.join("/")
82
+ def status_is_successful?(code)
83
+ (code >= 200) && (code < 400)
220
84
  end
221
85
 
222
- def log_data(time, request, response)
223
- { :time => time,
224
- :request => {
225
- :method => request.method,
226
- :url => request.path,
227
- :headers => sane_headers(request)
228
- },
229
- :response => {
230
- :code => response.code,
231
- :headers => sane_headers(response)
232
- }
233
- }
86
+ def handle_response(response, options, request)
87
+ if status_is_successful?(response[:status].to_i)
88
+ handle_successful_response(response, options)
89
+ else
90
+ handle_error_response(response, request)
91
+ end
234
92
  end
235
93
 
236
- def log_line(io, data)
237
- io.printf(
238
- "[%s] %0.3fs %6s -> %d %s\n",
239
- Time.now.strftime("%F %T"),
240
- data[:time],
241
- data[:request][:method].to_s.upcase,
242
- data[:response][:code],
243
- data[:request][:url])
94
+ def handle_successful_response(response, options)
95
+ if options[:return_response]
96
+ response
97
+ elsif options[:accept] == :json
98
+ parse_json(response[:body])
99
+ else
100
+ response[:body]
101
+ end
244
102
  end
245
103
 
246
- def log_request(time, request, response)
247
- return unless @log
248
-
249
- data = log_data(time, request, response)
250
-
251
- case @log
252
- when IO
253
- log_line(@log, data)
254
- return
255
- when String
256
- if File.exists?(@log)
257
- log = File.readlines(@log).last(LOG_LENGTH - 1)
258
- elsif !File.exists?(File.dirname(@log))
259
- FileUtils.mkdir_p(File.dirname(@log))
260
- end
104
+ def handle_error_response(response, request)
105
+ body_json = parse_json(response[:body])
106
+ body_code = body_json && body_json[:code]
107
+ code = body_code || response[:status].to_i
261
108
 
262
- File.open(@log, "w") do |io|
263
- log.each { |l| io.print l } if log
264
- log_line(io, data)
265
- end
266
-
267
- return
268
- end
269
-
270
- if @log.respond_to?(:call)
271
- @log.call(data)
272
- return
109
+ if body_code
110
+ error_class = CFoundry::APIError.error_classes[body_code] || CFoundry::APIError
111
+ raise error_class.new(body_json[:description], body_code, request, response)
273
112
  end
274
113
 
275
- if @log.respond_to?(:<<)
276
- @log << data
277
- return
114
+ case code
115
+ when 404
116
+ raise CFoundry::NotFound.new(nil, code, request, response)
117
+ when 403
118
+ raise CFoundry::Denied.new(nil, code, request, response)
119
+ else
120
+ raise CFoundry::BadResponse.new(nil, code, request, response)
278
121
  end
279
122
  end
280
123
 
281
- def print_request(request)
282
- $stderr.puts ">>>"
283
- $stderr.puts request_trace(request)
284
- end
285
-
286
- def print_response(response)
287
- $stderr.puts response_trace(response)
288
- $stderr.puts "<<<"
289
- end
290
-
291
- def print_backtrace(locs)
292
- return unless @backtrace
293
-
294
- interesting_locs = locs.drop_while { |loc|
295
- loc =~ /\/(cfoundry\/|restclient\/|net\/http)/
296
- }
297
-
298
- $stderr.puts "--- backtrace:"
299
-
300
- $stderr.puts "... (boring)" unless locs == interesting_locs
301
-
302
- trimmed_locs = interesting_locs[0..5]
303
-
304
- trimmed_locs.each do |loc|
305
- $stderr.puts "=== #{loc}"
124
+ def normalize_arguments(args)
125
+ if args.last.is_a?(Hash)
126
+ options = args.pop
127
+ else
128
+ options = {}
306
129
  end
307
130
 
308
- $stderr.puts "... (trimmed)" unless trimmed_locs == interesting_locs
131
+ [normalize_path(args), options]
309
132
  end
310
133
 
311
- def handle_response(response, accept, request)
312
- case response
313
- when Net::HTTPSuccess, Net::HTTPRedirection
314
- accept == :json ? parse_json(response.body) : response.body
315
-
316
- when Net::HTTPNotFound
317
- raise CFoundry::NotFound.new(request, response)
318
-
319
- when Net::HTTPForbidden
320
- raise CFoundry::Denied.new(request, response)
134
+ URI_ENCODING_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
321
135
 
322
- else
323
- raise CFoundry::BadResponse.new(request, response)
136
+ def normalize_path(segments)
137
+ if segments.size == 1 && segments.first =~ /^\//
138
+ segments.first
139
+ else
140
+ segments.flatten.collect { |x|
141
+ URI.encode(x.to_s, URI_ENCODING_PATTERN)
142
+ }.join("/")
324
143
  end
325
144
  end
326
145
 
327
- def sane_headers(obj)
328
- hds = {}
329
-
330
- obj.each_header do |k, v|
331
- hds[k] = v
146
+ def parse_json(x)
147
+ if x.empty?
148
+ raise MultiJson::DecodeError.new("Empty JSON string", [], "")
149
+ else
150
+ MultiJson.load(x, :symbolize_keys => true)
332
151
  end
333
-
334
- hds
152
+ rescue MultiJson::DecodeError
153
+ nil
335
154
  end
336
155
  end
337
156
  end