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