vagrant_cloud 2.0.0 → 3.0.1

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.
@@ -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