cfoundry 0.4.21 → 0.5.0

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 (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