vagrant_cloud 2.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,163 @@
1
+ module VagrantCloud
2
+ class Box
3
+ class Version < Data::Mutable
4
+ attr_reader :box
5
+ attr_required :version
6
+ attr_optional :status, :description_html, :description_markdown,
7
+ :created_at, :updated_at, :number, :providers, :description
8
+
9
+ attr_mutable :description
10
+
11
+ def initialize(box:, **opts)
12
+ if !box.is_a?(Box)
13
+ raise TypeError, "Expecting type `#{Box.name}` but received `#{box.class.name}`"
14
+ end
15
+ @box = box
16
+ opts[:providers] = Array(opts[:providers]).map do |provider|
17
+ if provider.is_a?(Provider)
18
+ provider
19
+ else
20
+ Provider.load(version: self, **provider)
21
+ end
22
+ end
23
+ super(opts)
24
+ clean!
25
+ end
26
+
27
+ # Delete this version
28
+ #
29
+ # @return [nil]
30
+ # @note This will delete the version, and all providers
31
+ def delete
32
+ if exist?
33
+ box.organization.account.client.box_version_delete(
34
+ username: box.username,
35
+ name: box.name,
36
+ version: version
37
+ )
38
+ # Remove self from box
39
+ v = box.versions.dup
40
+ v.delete(self)
41
+ box.clean(data: {versions: v})
42
+ end
43
+ nil
44
+ end
45
+
46
+ # Release this version
47
+ #
48
+ # @return [self]
49
+ def release
50
+ if released?
51
+ raise Error::BoxError::VersionStatusChangeError,
52
+ "Version #{version} is already released for box #{box.tag}"
53
+ end
54
+ if !exist?
55
+ raise Error::BoxError::VersionStatusChangeError,
56
+ "Version #{version} for box #{box.tag} must be saved before release"
57
+ end
58
+ result = box.organization.account.client.box_version_release(
59
+ username: box.username,
60
+ name: box.name,
61
+ version: version
62
+ )
63
+ clean(data: result, only: :status)
64
+ self
65
+ end
66
+
67
+ # Revoke this version
68
+ #
69
+ # @return [self]
70
+ def revoke
71
+ if !released?
72
+ raise Error::BoxError::VersionStatusChangeError,
73
+ "Version #{version} is not yet released for box #{box.tag}"
74
+ end
75
+ result = box.organization.account.client.box_version_revoke(
76
+ username: box.username,
77
+ name: box.name,
78
+ version: version
79
+ )
80
+ clean(data: result, only: :status)
81
+ self
82
+ end
83
+
84
+ # @return [Boolean]
85
+ def released?
86
+ status == "active"
87
+ end
88
+
89
+ # Add a new provider for this version
90
+ #
91
+ # @param [String] pname Name of provider
92
+ # @return [Provider]
93
+ def add_provider(pname)
94
+ if providers.any? { |p| p.name == pname }
95
+ raise Error::BoxError::VersionProviderExistsError,
96
+ "Provider #{pname} already exists for box #{box.tag} version #{version}"
97
+ end
98
+ pv = Provider.new(version: self, name: pname)
99
+ clean(data: {providers: providers + [pv]})
100
+ pv
101
+ end
102
+
103
+ # Check if this instance is dirty
104
+ #
105
+ # @param [Boolean] deep Check nested instances
106
+ # @return [Boolean] instance is dirty
107
+ def dirty?(key=nil, deep: false)
108
+ if key
109
+ super(key)
110
+ else
111
+ d = super() || !exist?
112
+ if deep && !d
113
+ d = providers.any? { |p| p.dirty?(deep: true) }
114
+ end
115
+ d
116
+ end
117
+ end
118
+
119
+ # @return [Boolean] version exists remotely
120
+ def exist?
121
+ !!created_at
122
+ end
123
+
124
+ # Save the version if any changes have been made
125
+ #
126
+ # @return [self]
127
+ def save
128
+ save_version if dirty?
129
+ save_providers if dirty?(deep: true)
130
+ self
131
+ end
132
+
133
+ protected
134
+
135
+ # Save the version
136
+ #
137
+ # @return [self]
138
+ def save_version
139
+ params = {
140
+ username: box.username,
141
+ name: box.name,
142
+ version: version,
143
+ description: description
144
+ }
145
+ if exist?
146
+ result = box.organization.account.client.box_version_update(**params)
147
+ else
148
+ result = box.organization.account.client.box_version_create(**params)
149
+ end
150
+ clean(data: result, ignores: :providers)
151
+ self
152
+ end
153
+
154
+ # Save the providers if any require saving
155
+ #
156
+ # @return [self]
157
+ def save_providers
158
+ Array(providers).map(&:save)
159
+ self
160
+ end
161
+ end
162
+ end
163
+ end
@@ -1,60 +1,470 @@
1
- require 'json'
2
-
3
1
  module VagrantCloud
4
2
  class Client
3
+ include Logger
5
4
  # Base Vagrant Cloud API URL
6
- URL_BASE = 'https://vagrantcloud.com/api/v1'.freeze
7
- attr_accessor :access_token
8
-
9
- # @param [String] access_token - token used to authenticate API requests
10
- # @param [String] url_base - URL used to make API requests
11
- def initialize(access_token = nil, url_base = nil)
12
- if url_base
13
- @url_base = url_base
14
- else
15
- @url_base = URL_BASE
16
- end
5
+ DEFAULT_URL = 'https://vagrantcloud.com/api/v1'.freeze
6
+ # Valid methods that can be retried
7
+ IDEMPOTENT_METHODS = [:get, :head].freeze
8
+ # Number or allowed retries
9
+ IDEMPOTENT_RETRIES = 3
10
+ # Number of seconds to wait between retries
11
+ IDEMPOTENT_RETRY_INTERVAL = 2
12
+ # Methods which require query parameters
13
+ QUERY_PARAMS_METHODS = [:get, :head, :delete].freeze
14
+ # Default instrumentor
15
+ DEFAULT_INSTRUMENTOR = Instrumentor::Collection.new
17
16
 
18
- @access_token = access_token
17
+ # @return [Instrumentor::Collection]
18
+ def self.instrumentor
19
+ DEFAULT_INSTRUMENTOR
19
20
  end
20
21
 
21
- # @param [String] method
22
- # @param [String] path
23
- # @param [Hash] params
24
- # @param [String] token
25
- # @return [Hash]
26
- def request(method, path, params = {}, token = nil)
27
- headers = {}
22
+ # @return [String] Access token for Vagrant Cloud
23
+ attr_reader :access_token
24
+ # @return [String] Base request path
25
+ attr_reader :path_base
26
+ # @return [String] URL for initializing connection
27
+ attr_reader :url_base
28
+ # @return [Integer] Number of retries on idempotent requests
29
+ attr_reader :retry_count
30
+ # @return [Integer] Number of seconds to wait between requests
31
+ attr_reader :retry_interval
32
+ # @return [Instrumentor::Collection] Instrumentor in use
33
+ attr_reader :instrumentor
34
+
35
+ # Create a new Client instance
36
+ #
37
+ # @param [String] access_token Authentication token for API requests
38
+ # @param [String] url_base URL used to make API requests
39
+ # @param [Integer] retry_count Number of retries on idempotent requests
40
+ # @param [Integer] retry_interval Number of seconds to wait between requests
41
+ # @param [Instrumentor::Core] instrumentor Instrumentor to use
42
+ # @return [Client]
43
+ def initialize(access_token: nil, url_base: nil, retry_count: nil, retry_interval: nil, instrumentor: nil)
44
+ url_base = DEFAULT_URL if url_base.nil?
45
+ remote_url = URI.parse(url_base)
46
+ @url_base = "#{remote_url.scheme}://#{remote_url.host}"
47
+ @path_base = remote_url.path
48
+ @access_token = access_token.dup.freeze if access_token
49
+ if !@access_token && ENV["VAGRANT_CLOUD_TOKEN"]
50
+ @access_token = ENV["VAGRANT_CLOUD_TOKEN"].dup.freeze
51
+ end
52
+ @retry_count = retry_count.nil? ? IDEMPOTENT_RETRIES : retry_count.to_i
53
+ @retry_interval = retry_interval.nil? ? IDEMPOTENT_RETRY_INTERVAL : retry_interval.to_i
54
+ @instrumentor = instrumentor.nil? ? Instrumentor::Collection.new : instrumentor
55
+ headers = {}.tap do |h|
56
+ h["Accept"] = "application/json"
57
+ h["Authorization"] = "Bearer #{@access_token}" if @access_token
58
+ h["Content-Type"] = "application/json"
59
+ end
60
+ @connection_lock = Mutex.new
61
+ @connection = Excon.new(url_base,
62
+ headers: headers,
63
+ instrumentor: @instrumentor
64
+ )
65
+ end
28
66
 
29
- if token
30
- headers['Authorization'] = "Bearer #{token}"
31
- elsif @access_token
32
- headers['Authorization'] = "Bearer #{@access_token}"
67
+ # Use the remote connection
68
+ #
69
+ # @param [Boolean] wait Wait for the connection to be available
70
+ # @yieldparam [Excon::Connection]
71
+ # @return [Object]
72
+ def with_connection(wait: true)
73
+ raise ArgumentError,
74
+ "Block expected but no block given" if !block_given?
75
+ if !wait
76
+ raise Error::ClientError::ConnectionLockedError,
77
+ "Connection is currently locked" if !@connection_lock.try_lock
78
+ begin
79
+ yield @connection
80
+ ensure
81
+ @connection_lock.unlock
82
+ end
83
+ else
84
+ @connection_lock.synchronize { yield @connection }
33
85
  end
86
+ end
34
87
 
35
- headers['Accept'] = 'application/json'
88
+ # Send a request
89
+ # @param [String, Symbol] method Request method
90
+ # @param [String, URI] path Path of request
91
+ # @param [Hash] params Parameters to send with request
92
+ # @return [Hash]
93
+ def request(path:, method: :get, params: {})
94
+ if !path.start_with?(path_base)
95
+ # Build the full path for the request and clean it
96
+ path = [path_base, path].compact.join("/").gsub(/\/{2,}/, "/")
97
+ end
98
+ method = method.to_s.downcase.to_sym
36
99
 
100
+ # Build base request parameters
37
101
  request_params = {
38
102
  method: method,
39
- url: @url_base + path,
40
- headers: headers,
41
- ssl_version: 'TLSv1'
103
+ path: path,
104
+ expects: [200, 201, 204]
42
105
  }
43
106
 
44
- if ['get', 'head', 'delete'].include?(method.downcase)
45
- headers[:params] = params
46
- else
47
- request_params[:payload] = params
107
+ # If this is an idempotent request allow it to retry on failure
108
+ if IDEMPOTENT_METHODS.include?(method)
109
+ request_params[:idempotent] = true
110
+ request_params[:retry_limit] = retry_count
111
+ request_params[:retry_interval] = retry_interval
48
112
  end
49
113
 
50
- begin
51
- result = RestClient::Request.execute(request_params)
114
+ # If parameters are provided, set them in the expected location
115
+ if !params.empty?
116
+ # Copy the parameters so we can freely modify them
117
+ params = clean_parameters(params)
118
+
119
+ if QUERY_PARAMS_METHODS.include?(method)
120
+ request_params[:query] = params
121
+ else
122
+ request_params[:body] = JSON.dump(params)
123
+ end
124
+ end
125
+
126
+ # Set a request ID so we can track request/responses
127
+ request_params[:headers] = {"X-Request-Id" => SecureRandom.uuid}
128
+
129
+ result = with_connection { |c| c.request(request_params) }
130
+ parse_json(result.body)
131
+ end
132
+
133
+ # Clone this client to create a new instance
134
+ #
135
+ # @param [String] access_token Authentication token for API requests
136
+ # @return [Client]
137
+ def clone(access_token: nil)
138
+ self.class.new(access_token: access_token, url_base: url_base,
139
+ retry_count: retry_count, retry_interval: retry_interval
140
+ )
141
+ end
142
+
143
+ # Submit a search on Vagrant Cloud
144
+ #
145
+ # @param [String] query Search query
146
+ # @param [String] provider Limit results to only this provider
147
+ # @param [String] sort Field to sort results ("downloads", "created", or "updated")
148
+ # @param [String] order Order to return sorted result ("desc" or "asc")
149
+ # @param [Integer] limit Number of results to return
150
+ # @param [Integer] page Page number of results to return
151
+ # @return [Hash]
152
+ def search(query: Data::Nil, provider: Data::Nil, sort: Data::Nil, order: Data::Nil, limit: Data::Nil, page: Data::Nil)
153
+ params = {
154
+ q: query,
155
+ provider: provider,
156
+ sort: sort,
157
+ order: order,
158
+ limit: limit,
159
+ page: page
160
+ }
161
+ request(method: :get, path: "search", params: params)
162
+ end
163
+
164
+ # Create a new access token
165
+ #
166
+ # @param [String] username Vagrant Cloud username
167
+ # @param [String] password Vagrant Cloud password
168
+ # @param [String] description Description of token
169
+ # @param [String] code 2FA code
170
+ # @return [Hash]
171
+ def authentication_token_create(username:, password:, description: Data::Nil, code: Data::Nil)
172
+ params = {
173
+ user: {
174
+ login: username,
175
+ password: password
176
+ },
177
+ token: {
178
+ description: description
179
+ },
180
+ two_factor: {
181
+ code: code
182
+ }
183
+ }
184
+ request(method: :post, path: "authenticate", params: params)
185
+ end
186
+
187
+ # Delete the token currently in use
188
+ #
189
+ # @return [Hash] empty
190
+ def authentication_token_delete
191
+ request(method: :delete, path: "authenticate")
192
+ end
193
+
194
+ # Request a 2FA code is sent
195
+ #
196
+ # @param [String] username Vagrant Cloud username
197
+ # @param [String] password Vagrant Cloud password
198
+ # @param [String] delivery_method Delivery method of 2FA
199
+ # @param [String] password Account password
200
+ # @return [Hash]
201
+ def authentication_request_2fa_code(username:, password:, delivery_method:)
202
+ params = {
203
+ two_factor: {
204
+ delivery_method: delivery_method
205
+ },
206
+ user: {
207
+ login: username,
208
+ password: password
209
+ }
210
+ }
211
+
212
+ request(method: :post, path: "two-factor/request-code", params: params)
213
+ end
214
+
215
+ # Validate the current token
216
+ #
217
+ # @return [Hash] emtpy
218
+ def authentication_token_validate
219
+ request(method: :get, path: "authenticate")
220
+ end
221
+
222
+ # Get an organization
223
+ #
224
+ # @param [String] name Name of organization
225
+ # @return [Hash] organization information
226
+ def organization_get(name:)
227
+ request(method: :get, path: "user/#{name}")
228
+ end
229
+
230
+ # Get an existing box
231
+ #
232
+ # @param [String] username Username/organization name to create box under
233
+ # @param [String] name Box name
234
+ # @return [Hash] box information
235
+ def box_get(username:, name:)
236
+ request(method: :get, path: "/box/#{username}/#{name}")
237
+ end
238
+
239
+ # Create a new box
240
+ #
241
+ # @param [String] username Username/organization name to create box under
242
+ # @param [String] name Box name
243
+ # @param [String] short_description Short description of box
244
+ # @param [String] description Long description of box (markdown supported)
245
+ # @param [Boolean] is_private Set if box is private
246
+ # @return [Hash] box information
247
+ def box_create(username:, name:, short_description: Data::Nil, description: Data::Nil, is_private: Data::Nil)
248
+ request(method: :post, path: '/boxes', params: {
249
+ username: username,
250
+ name: name,
251
+ short_description: short_description,
252
+ description: description,
253
+ is_private: is_private
254
+ })
255
+ end
256
+
257
+ # Update an existing box
258
+ #
259
+ # @param [String] username Username/organization name to create box under
260
+ # @param [String] name Box name
261
+ # @param [String] short_description Short description of box
262
+ # @param [String] description Long description of box (markdown supported)
263
+ # @param [Boolean] is_private Set if box is private
264
+ # @return [Hash] box information
265
+ def box_update(username:, name:, short_description: Data::Nil, description: Data::Nil, is_private: Data::Nil)
266
+ params = {
267
+ short_description: short_description,
268
+ description: description,
269
+ is_private: is_private
270
+ }
271
+ request(method: :put, path: "/box/#{username}/#{name}", params: params)
272
+ end
273
+
274
+ # Delete an existing box
275
+ #
276
+ # @param [String] username Username/organization name to create box under
277
+ # @param [String] name Box name
278
+ # @return [Hash] box information
279
+ def box_delete(username:, name:)
280
+ request(method: :delete, path: "/box/#{username}/#{name}")
281
+ end
282
+
283
+ # Get an existing box version
284
+ #
285
+ # @param [String] username Username/organization name to create box under
286
+ # @param [String] name Box name
287
+ # @param [String] version Box version
288
+ # @return [Hash] box version information
289
+ def box_version_get(username:, name:, version:)
290
+ request(method: :get, path: "/box/#{username}/#{name}/version/#{version}")
291
+ end
292
+
293
+ # Create a new box version
294
+ #
295
+ # @param [String] username Username/organization name to create box under
296
+ # @param [String] name Box name
297
+ # @param [String] version Box version
298
+ # @param [String] description Box description
299
+ # @return [Hash] box version information
300
+ def box_version_create(username:, name:, version:, description: Data::Nil)
301
+ request(method: :post, path: "/box/#{username}/#{name}/versions", params: {
302
+ version: {
303
+ version: version,
304
+ description: description
305
+ }
306
+ })
307
+ end
308
+
309
+ # Update an existing box version
310
+ #
311
+ # @param [String] username Username/organization name to create box under
312
+ # @param [String] name Box name
313
+ # @param [String] version Box version
314
+ # @param [String] description Box description
315
+ # @return [Hash] box version information
316
+ def box_version_update(username:, name:, version:, description: Data::Nil)
317
+ params = {
318
+ version: {
319
+ version: version,
320
+ description: description
321
+ }
322
+ }
323
+ request(method: :put, path: "/box/#{username}/#{name}/version/#{version}", params: params)
324
+ end
325
+
326
+ # Delete an existing box version
327
+ #
328
+ # @param [String] username Username/organization name to create box under
329
+ # @param [String] name Box name
330
+ # @param [String] version Box version
331
+ # @return [Hash] box version information
332
+ def box_version_delete(username:, name:, version:)
333
+ request(method: :delete, path: "/box/#{username}/#{name}/version/#{version}")
334
+ end
335
+
336
+ # Release an existing box version
337
+ #
338
+ # @param [String] username Username/organization name to create box under
339
+ # @param [String] name Box name
340
+ # @param [String] version Box version
341
+ # @return [Hash] box version information
342
+ def box_version_release(username:, name:, version:)
343
+ request(method: :put, path: "/box/#{username}/#{name}/version/#{version}/release")
344
+ end
345
+
346
+ # Revoke an existing box version
347
+ #
348
+ # @param [String] username Username/organization name to create box under
349
+ # @param [String] name Box name
350
+ # @param [String] version Box version
351
+ # @return [Hash] box version information
352
+ def box_version_revoke(username:, name:, version:)
353
+ request(method: :put, path: "/box/#{username}/#{name}/version/#{version}/revoke")
354
+ end
355
+
356
+ # Get an existing box version provider
357
+ #
358
+ # @param [String] username Username/organization name to create box under
359
+ # @param [String] name Box name
360
+ # @param [String] version Box version
361
+ # @param [String] provider Provider name
362
+ # @return [Hash] box version provider information
363
+ def box_version_provider_get(username:, name:, version:, provider:)
364
+ request(method: :get, path: "/box/#{username}/#{name}/version/#{version}/provider/#{provider}")
365
+ end
366
+
367
+ # Create a new box version provider
368
+ #
369
+ # @param [String] username Username/organization name to create box under
370
+ # @param [String] name Box name
371
+ # @param [String] version Box version
372
+ # @param [String] provider Provider name
373
+ # @param [String] url Remote URL for box download
374
+ # @return [Hash] box version provider information
375
+ def box_version_provider_create(username:, name:, version:, provider:, url: Data::Nil, checksum: Data::Nil, checksum_type: Data::Nil)
376
+ request(method: :post, path: "/box/#{username}/#{name}/version/#{version}/providers", params: {
377
+ provider: {
378
+ name: provider,
379
+ url: url,
380
+ checksum: checksum,
381
+ checksum_type: checksum_type
382
+ }
383
+ })
384
+ end
385
+
386
+ # Update an existing box version provider
387
+ #
388
+ # @param [String] username Username/organization name to create box under
389
+ # @param [String] name Box name
390
+ # @param [String] version Box version
391
+ # @param [String] provider Provider name
392
+ # @param [String] url Remote URL for box download
393
+ # @return [Hash] box version provider information
394
+ def box_version_provider_update(username:, name:, version:, provider:, url: Data::Nil, checksum: Data::Nil, checksum_type: Data::Nil)
395
+ params = {
396
+ provider: {
397
+ name: provider,
398
+ url: url,
399
+ checksum: checksum,
400
+ checksum_type: checksum_type
401
+ }
402
+ }
403
+ request(method: :put, path: "/box/#{username}/#{name}/version/#{version}/provider/#{provider}", params: params)
404
+ end
405
+
406
+ # Delete an existing box version provider
407
+ #
408
+ # @param [String] username Username/organization name to create box under
409
+ # @param [String] name Box name
410
+ # @param [String] version Box version
411
+ # @param [String] provider Provider name
412
+ # @return [Hash] box version provider information
413
+ def box_version_provider_delete(username:, name:, version:, provider:)
414
+ request(method: :delete, path: "/box/#{username}/#{name}/version/#{version}/provider/#{provider}")
415
+ end
416
+
417
+ # Upload a box asset for an existing box version provider
418
+ #
419
+ # @param [String] username Username/organization name to create box under
420
+ # @param [String] name Box name
421
+ # @param [String] version Box version
422
+ # @param [String] provider Provider name
423
+ # @return [Hash] box version provider upload information (contains upload_path entry)
424
+ def box_version_provider_upload(username:, name:, version:, provider:)
425
+ request(method: :get, path: "/box/#{username}/#{name}/version/#{version}/provider/#{provider}/upload")
426
+ end
427
+
428
+ # Upload a box asset directly to the backend storage for an existing box version provider
429
+ #
430
+ # @param [String] username Username/organization name to create box under
431
+ # @param [String] name Box name
432
+ # @param [String] version Box version
433
+ # @param [String] provider Provider name
434
+ # @return [Hash] box version provider upload information (contains upload_path and callback entries)
435
+ def box_version_provider_upload_direct(username:, name:, version:, provider:)
436
+ request(method: :get, path: "/box/#{username}/#{name}/version/#{version}/provider/#{provider}/upload/direct")
437
+ end
438
+
439
+ protected
440
+
441
+ # Parse a string of JSON
442
+ #
443
+ # @param [String] string String of JSON data
444
+ # @return [Object]
445
+ # @note All keys are symbolized when parsed
446
+ def parse_json(string)
447
+ return {} if string.empty?
448
+ JSON.parse(string, symbolize_names: true)
449
+ end
52
450
 
53
- parsed_result = JSON.parse(result)
54
- parsed_result
55
- rescue RestClient::ExceptionWithResponse => e
56
- raise ClientError.new(e.message, e.http_body, e.http_code)
451
+ # Remove any values that have a default value set
452
+ #
453
+ # @param [Object] item Item to clean
454
+ # @return [Object] cleaned item
455
+ def clean_parameters(item)
456
+ case item
457
+ when Array
458
+ item = item.find_all { |i| i != Data::Nil }
459
+ item.map! { |i| clean_parameters(i) }
460
+ when Hash
461
+ item = item.dup
462
+ item.delete_if{ |_,v| v == Data::Nil }
463
+ item.keys.each do |k|
464
+ item[k] = clean_parameters(item[k])
465
+ end
57
466
  end
467
+ item
58
468
  end
59
469
  end
60
470
  end