azure-armrest 0.4.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGES +22 -0
- data/README.md +7 -3
- data/lib/azure/armrest.rb +11 -5
- data/lib/azure/armrest/armrest_service.rb +14 -6
- data/lib/azure/armrest/billing/usage_service.rb +1 -8
- data/lib/azure/armrest/configuration.rb +48 -16
- data/lib/azure/armrest/insights/diagnostic_service.rb +1 -1
- data/lib/azure/armrest/insights/event_service.rb +1 -13
- data/lib/azure/armrest/insights/metrics_service.rb +1 -4
- data/lib/azure/armrest/resource_group_based_service.rb +2 -2
- data/lib/azure/armrest/resource_group_service.rb +1 -2
- data/lib/azure/armrest/resource_provider_service.rb +2 -2
- data/lib/azure/armrest/resource_service.rb +8 -6
- data/lib/azure/armrest/subscription_service.rb +1 -1
- data/lib/azure/armrest/version.rb +1 -1
- data/lib/azure/armrest/virtual_machine_extension_service.rb +1 -2
- data/lib/azure/armrest/virtual_machine_image_service.rb +1 -9
- data/lib/azure/armrest/virtual_machine_service.rb +11 -24
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a888f86dfd49210ca1ce4232968d2449f8ee1a2
|
4
|
+
data.tar.gz: 8a19fb58fe560e115b0153e843f0eaaf8329c61d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc6c79041f6c46dcad609fd72e2e5c719c2b84cea7e0fc2cf10fd3da4bc789f92bfcdcddf69748fbfe1e9e837fc001ab607c1d6bc38d2ce0752639284f7acbb7
|
7
|
+
data.tar.gz: 55fd98f1b9af93b7e84b6eaef6b98d5814a31531518ceb00c5c617e2242c547d74ab40c3f4d073bab98d2feb0ca5e09cf1292f45a982bf49532f60817426cc90
|
data/CHANGES
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
= 0.4.1 - 16-Dec-2016
|
2
|
+
* The Configuration#environment accessor was added. As of this release you
|
3
|
+
can specify 'usgov' as an option to the constructor, and the appropriate
|
4
|
+
resource and authority endpoints will be used instead of the public ones.
|
5
|
+
* Added the #authority_url and #resource_url accessors to the Configuration
|
6
|
+
class. Use wisely.
|
7
|
+
* The Armrest::COMMON constant was removed because the resource isn't actually
|
8
|
+
constant. Instead use the authority_url or resource_url methods. This was
|
9
|
+
really only meant for internal use anyway.
|
10
|
+
* Added the Armrest::USGOV_RESOURCE and Armrest::USGOV_AUTHORITY constants.
|
11
|
+
* Fixed a bug in the VirtualMachineService#delete_associated_resources where
|
12
|
+
the method would fail if you requested network security group deletion but
|
13
|
+
there was no associated network security group.
|
14
|
+
* Fixed an issue in the ArmrestService#poll method where a 202 response might
|
15
|
+
not actually have a body. In that case, it is treated as success.
|
16
|
+
* Fixed a logic bug in the ArmrestService#wait method, and added the option
|
17
|
+
to specify 0 (infinity) for the wait time.
|
18
|
+
* Added the ArmrestService#log method, and the Configuration.log= method now
|
19
|
+
automatically converts the argument to a Logger instance if it's not already.
|
20
|
+
* The ArmrestService#base_url method was altered to include subscription
|
21
|
+
information.
|
22
|
+
|
1
23
|
= 0.4.0 - 8-Dec-2016
|
2
24
|
* The Configuration constructor no longer requires a subscription ID. However,
|
3
25
|
the presence of a subscription ID is still required for almost all Service
|
data/README.md
CHANGED
@@ -43,9 +43,13 @@ end
|
|
43
43
|
|
44
44
|
## Subscriptions
|
45
45
|
|
46
|
-
As of version 0.
|
47
|
-
|
48
|
-
|
46
|
+
As of version 0.4.0 you a subscription ID is not longer strictly necessary in
|
47
|
+
the Configuration constructor, but almost all service classes require it in
|
48
|
+
their own constructor. Only the SubscriptionService class does not.
|
49
|
+
|
50
|
+
In version 0.3.x the subscription ID was mandatory. Prior to 0.3.x, if you did
|
51
|
+
not provide a subscription ID in your configuration object, then the first
|
52
|
+
subscription ID returned from a REST call would be used.
|
49
53
|
|
50
54
|
## Notes
|
51
55
|
|
data/lib/azure/armrest.rb
CHANGED
@@ -11,14 +11,20 @@ module Azure
|
|
11
11
|
# The Armrest module mostly serves as a namespace, but also contains any
|
12
12
|
# common constants shared by subclasses.
|
13
13
|
module Armrest
|
14
|
-
# The default Azure resource
|
14
|
+
# The default (public) Azure resource
|
15
15
|
RESOURCE = "https://management.azure.com/"
|
16
16
|
|
17
|
-
# The
|
18
|
-
|
17
|
+
# The resource for US Government clients
|
18
|
+
USGOV_RESOURCE = "https://management.core.usgovcloudapi.net/"
|
19
19
|
|
20
|
-
#
|
21
|
-
|
20
|
+
# The default (public) authority resource
|
21
|
+
AUTHORITY = "https://login.microsoftonline.com/"
|
22
|
+
|
23
|
+
# The authority for US Government clients
|
24
|
+
USGOV_AUTHORITY = "https://login-us.microsoftonline.com/"
|
25
|
+
|
26
|
+
# Environment string used to indicate US Government
|
27
|
+
USGOV_ENVIRONMENT = 'usgov'
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
@@ -12,7 +12,7 @@ module Azure
|
|
12
12
|
|
13
13
|
alias configuration armrest_configuration
|
14
14
|
|
15
|
-
# Base url used for REST calls.
|
15
|
+
# Base url with subscription information used for most REST calls.
|
16
16
|
attr_accessor :base_url
|
17
17
|
|
18
18
|
# Provider for service specific API calls
|
@@ -46,7 +46,7 @@ module Azure
|
|
46
46
|
end
|
47
47
|
|
48
48
|
# Base URL used for REST calls. Modify within method calls as needed.
|
49
|
-
@base_url =
|
49
|
+
@base_url = File.join(configuration.resource_url, 'subscriptions', configuration.subscription_id)
|
50
50
|
|
51
51
|
set_service_api_version(options, service_name)
|
52
52
|
end
|
@@ -165,7 +165,11 @@ module Azure
|
|
165
165
|
def poll(response)
|
166
166
|
return 'Succeeded' if [200, 201].include?(response.response_code)
|
167
167
|
url = response.try(:azure_asyncoperation) || response.try(:location)
|
168
|
-
|
168
|
+
response = rest_get(url).body
|
169
|
+
unless response.blank?
|
170
|
+
status = JSON.parse(response)['status']
|
171
|
+
end
|
172
|
+
status || 'Succeeded' # assume succeeded otherwise the wait method may hang
|
169
173
|
end
|
170
174
|
|
171
175
|
# Wait for the given +response+ to return a status of 'Succeeded', up
|
@@ -175,7 +179,7 @@ module Azure
|
|
175
179
|
#
|
176
180
|
# Internally this will poll the response header every :retry_after
|
177
181
|
# seconds (or 10 seconds if that header isn't found), up to a maximum of
|
178
|
-
# 60 seconds by default.
|
182
|
+
# 60 seconds by default. There is no timeout limit if +max_time+ is 0.
|
179
183
|
#
|
180
184
|
# For most resources the +max_time+ argument should be more than sufficient.
|
181
185
|
# Certain resources, such as virtual machines, could take longer.
|
@@ -184,9 +188,9 @@ module Azure
|
|
184
188
|
sleep_time = response.respond_to?(:retry_after) ? response.retry_after.to_i : 10
|
185
189
|
total_time = 0
|
186
190
|
|
187
|
-
|
191
|
+
until (status = poll(response)) =~ /^succe/i # success or succeeded
|
188
192
|
total_time += sleep_time
|
189
|
-
break if total_time >= max_time
|
193
|
+
break if max_time > 0 && total_time >= max_time
|
190
194
|
sleep sleep_time
|
191
195
|
end
|
192
196
|
|
@@ -353,6 +357,10 @@ module Azure
|
|
353
357
|
def model_class
|
354
358
|
@model_class ||= Object.const_get(self.class.to_s.sub(/Service$/, ''))
|
355
359
|
end
|
360
|
+
|
361
|
+
def log(level = "info", msg)
|
362
|
+
RestClient.log.try(level, msg)
|
363
|
+
end
|
356
364
|
end # ArmrestService
|
357
365
|
end # Armrest
|
358
366
|
end # Azure
|
@@ -59,14 +59,7 @@ module Azure
|
|
59
59
|
private
|
60
60
|
|
61
61
|
def build_url(options = {})
|
62
|
-
url = File.join(
|
63
|
-
Azure::Armrest::COMMON_URI,
|
64
|
-
configuration.subscription_id,
|
65
|
-
'providers',
|
66
|
-
@provider,
|
67
|
-
'UsageAggregates'
|
68
|
-
)
|
69
|
-
|
62
|
+
url = File.join(base_url, 'providers', @provider, 'UsageAggregates')
|
70
63
|
url << "?api-version=#{@api_version}"
|
71
64
|
|
72
65
|
options.each do |key, value|
|
@@ -64,6 +64,15 @@ module Azure
|
|
64
64
|
# Maximum number of threads to use within methods that use Parallel for thread pooling.
|
65
65
|
attr_accessor :max_threads
|
66
66
|
|
67
|
+
# The environment in which to acquire your token.
|
68
|
+
attr_reader :environment
|
69
|
+
|
70
|
+
# The authority URL used to acquire a valid token.
|
71
|
+
attr_accessor :resource_url
|
72
|
+
|
73
|
+
# The resource URL used to acquire a valid token.
|
74
|
+
attr_accessor :authority_url
|
75
|
+
|
67
76
|
# Yields a new Azure::Armrest::Configuration objects. Note that you must
|
68
77
|
# specify a client_id, client_key, tenant_id. The subscription_id is optional
|
69
78
|
# but should be specified in most cases. All other parameters are optional.
|
@@ -90,13 +99,15 @@ module Azure
|
|
90
99
|
def initialize(args)
|
91
100
|
# Use defaults, and override with provided arguments
|
92
101
|
options = {
|
93
|
-
:api_version
|
94
|
-
:accept
|
95
|
-
:content_type
|
96
|
-
:grant_type
|
97
|
-
:proxy
|
98
|
-
:ssl_version
|
99
|
-
:max_threads
|
102
|
+
:api_version => '2015-01-01',
|
103
|
+
:accept => 'application/json',
|
104
|
+
:content_type => 'application/json',
|
105
|
+
:grant_type => 'client_credentials',
|
106
|
+
:proxy => ENV['http_proxy'],
|
107
|
+
:ssl_version => 'TLSv1',
|
108
|
+
:max_threads => 10,
|
109
|
+
:authority_url => Azure::Armrest::AUTHORITY,
|
110
|
+
:resource_url => Azure::Armrest::RESOURCE
|
100
111
|
}.merge(args.symbolize_keys)
|
101
112
|
|
102
113
|
# Avoid thread safety issues for VCR testing.
|
@@ -184,19 +195,18 @@ module Azure
|
|
184
195
|
end
|
185
196
|
end
|
186
197
|
|
187
|
-
#
|
188
|
-
|
189
|
-
# We have to do a little extra work here to convert a possible
|
190
|
-
# file handle to a file name.
|
198
|
+
# Returns the logger instance. It might be initially set through a log
|
199
|
+
# file path, file handler, or already a logger instance.
|
191
200
|
#
|
192
201
|
def self.log
|
193
|
-
|
194
|
-
file || RestClient.log
|
202
|
+
RestClient.log
|
195
203
|
end
|
196
204
|
|
197
|
-
# Sets the log to +output+, which can be a file
|
205
|
+
# Sets the log to +output+, which can be a file, a file handle, or
|
206
|
+
# a logger instance
|
198
207
|
#
|
199
208
|
def self.log=(output)
|
209
|
+
output = Logger.new(output) unless output.kind_of?(Logger)
|
200
210
|
RestClient.log = output
|
201
211
|
end
|
202
212
|
|
@@ -208,6 +218,28 @@ module Azure
|
|
208
218
|
|
209
219
|
private
|
210
220
|
|
221
|
+
# Sets the environment to authenticate against. The environment
|
222
|
+
# must support ActiveDirectory.
|
223
|
+
#
|
224
|
+
def environment=(env)
|
225
|
+
return if env == environment
|
226
|
+
set_auth_and_resource_urls(env)
|
227
|
+
@environment = env
|
228
|
+
end
|
229
|
+
|
230
|
+
# Sets the authority_url and resource_url accessors depending on the
|
231
|
+
# environment.
|
232
|
+
#--
|
233
|
+
# Only two supported at the moment, but more likely to be added.
|
234
|
+
#
|
235
|
+
def set_auth_and_resource_urls(env)
|
236
|
+
case env.to_s.downcase
|
237
|
+
when Azure::Armrest::USGOV_ENVIRONMENT
|
238
|
+
@authority_url = Azure::Armrest::USGOV_AUTHORITY
|
239
|
+
@resource_url = Azure::Armrest::USGOV_RESOURCE
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
211
243
|
# Validate the subscription ID for the given credentials. Returns the
|
212
244
|
# subscription ID if valid.
|
213
245
|
#
|
@@ -275,7 +307,7 @@ module Azure
|
|
275
307
|
end
|
276
308
|
|
277
309
|
def fetch_token
|
278
|
-
token_url = File.join(
|
310
|
+
token_url = File.join(authority_url, tenant_id, 'oauth2/token')
|
279
311
|
|
280
312
|
response = JSON.parse(
|
281
313
|
ArmrestService.send(
|
@@ -288,7 +320,7 @@ module Azure
|
|
288
320
|
:grant_type => grant_type,
|
289
321
|
:client_id => client_id,
|
290
322
|
:client_secret => client_key,
|
291
|
-
:resource =>
|
323
|
+
:resource => resource_url
|
292
324
|
}
|
293
325
|
)
|
294
326
|
)
|
@@ -66,19 +66,7 @@ module Azure
|
|
66
66
|
private
|
67
67
|
|
68
68
|
def build_url(options = {})
|
69
|
-
|
70
|
-
|
71
|
-
url =
|
72
|
-
File.join(
|
73
|
-
Azure::Armrest::COMMON_URI,
|
74
|
-
sub_id,
|
75
|
-
'providers',
|
76
|
-
provider,
|
77
|
-
'eventtypes',
|
78
|
-
'management',
|
79
|
-
'values'
|
80
|
-
)
|
81
|
-
|
69
|
+
url = File.join(base_url, 'providers', provider, 'eventtypes', 'management', 'values')
|
82
70
|
url << "?api-version=#{@api_version}"
|
83
71
|
url << "&$filter=#{options[:filter]}" if options[:filter]
|
84
72
|
url << "&$select=#{options[:select]}" if options[:select]
|
@@ -34,11 +34,8 @@ module Azure
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def build_url(provider, resource_type, resource_name, resource_group, options)
|
37
|
-
sub_id = configuration.subscription_id
|
38
|
-
|
39
37
|
url = File.join(
|
40
|
-
|
41
|
-
sub_id,
|
38
|
+
base_url,
|
42
39
|
'resourceGroups',
|
43
40
|
resource_group,
|
44
41
|
'providers',
|
@@ -114,7 +114,7 @@ module Azure
|
|
114
114
|
api_version ||= configuration.api_version
|
115
115
|
service_name = info['subservice_name'] || info['service_name']
|
116
116
|
|
117
|
-
url = File.join(
|
117
|
+
url = File.join(configuration.resource_url, id_string) + "?api-version=#{api_version}"
|
118
118
|
|
119
119
|
model_class = SERVICE_NAME_MAP.fetch(service_name.downcase) do
|
120
120
|
raise ArgumentError, "unable to map service name #{service_name} to model"
|
@@ -203,7 +203,7 @@ module Azure
|
|
203
203
|
# arguments provided, and appends it with the api_version.
|
204
204
|
#
|
205
205
|
def build_url(resource_group = nil, *args)
|
206
|
-
url =
|
206
|
+
url = base_url
|
207
207
|
url = File.join(url, 'resourceGroups', resource_group) if resource_group
|
208
208
|
url = File.join(url, 'providers', @provider, @service_name)
|
209
209
|
url = File.join(url, *args) unless args.empty?
|
@@ -69,8 +69,7 @@ module Azure
|
|
69
69
|
private
|
70
70
|
|
71
71
|
def build_url(group = nil, *args)
|
72
|
-
|
73
|
-
url = File.join(Azure::Armrest::COMMON_URI, id, 'resourcegroups')
|
72
|
+
url = File.join(base_url, 'resourcegroups')
|
74
73
|
url = File.join(url, group) if group
|
75
74
|
url = File.join(url, *args) unless args.empty?
|
76
75
|
url << "?api-version=#{@api_version}"
|
@@ -61,7 +61,7 @@ module Azure
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def _list_all
|
64
|
-
url = File.join(
|
64
|
+
url = File.join(configuration.resource_url, 'providers')
|
65
65
|
url << "?api-version=#{@api_version}"
|
66
66
|
response = rest_get(url)
|
67
67
|
JSON.parse(response)['value']
|
@@ -127,7 +127,7 @@ module Azure
|
|
127
127
|
|
128
128
|
def build_url(namespace = nil, *args)
|
129
129
|
id = configuration.subscription_id
|
130
|
-
url = File.join(
|
130
|
+
url = File.join(base_url, 'providers')
|
131
131
|
url = File.join(url, namespace) if namespace
|
132
132
|
url = File.join(url, *args) unless args.empty?
|
133
133
|
url << "?api-version=#{@api_version}"
|
@@ -56,8 +56,12 @@ module Azure
|
|
56
56
|
#
|
57
57
|
def move(source_group, source_subscription = configuration.subscription_id)
|
58
58
|
url = File.join(
|
59
|
-
|
60
|
-
'
|
59
|
+
configuration.resource_url,
|
60
|
+
'subscriptions',
|
61
|
+
source_subscription,
|
62
|
+
'resourcegroups',
|
63
|
+
source_group,
|
64
|
+
'moveresources'
|
61
65
|
)
|
62
66
|
|
63
67
|
url << "?api-version=#{@api_version}"
|
@@ -90,12 +94,10 @@ module Azure
|
|
90
94
|
private
|
91
95
|
|
92
96
|
def build_url(resource_group = nil, options = {})
|
93
|
-
url = File.join(Azure::Armrest::COMMON_URI, configuration.subscription_id)
|
94
|
-
|
95
97
|
if resource_group
|
96
|
-
url = File.join(
|
98
|
+
url = File.join(base_url, 'resourceGroups', resource_group, 'resources')
|
97
99
|
else
|
98
|
-
url = File.join(
|
100
|
+
url = File.join(base_url, 'resources')
|
99
101
|
end
|
100
102
|
|
101
103
|
url << "?api-version=#{@api_version}"
|
@@ -98,15 +98,7 @@ module Azure
|
|
98
98
|
# arguments provided, and appends it with the api_version.
|
99
99
|
#
|
100
100
|
def build_url(location, *args)
|
101
|
-
url = File.join(
|
102
|
-
Azure::Armrest::COMMON_URI,
|
103
|
-
configuration.subscription_id,
|
104
|
-
'providers',
|
105
|
-
provider,
|
106
|
-
'locations',
|
107
|
-
location
|
108
|
-
)
|
109
|
-
|
101
|
+
url = File.join(base_url, 'providers', provider, 'locations', location)
|
110
102
|
url = File.join(url, *args) unless args.empty?
|
111
103
|
url << "?api-version=#{@api_version}"
|
112
104
|
end
|
@@ -200,9 +200,10 @@ module Azure
|
|
200
200
|
end
|
201
201
|
|
202
202
|
if options[:network_security_groups]
|
203
|
-
nic.properties.network_security_group
|
204
|
-
|
205
|
-
|
203
|
+
if nic.properties.respond_to?(:network_security_group)
|
204
|
+
nsg = get_associated_resource(nic.properties.network_security_group.id)
|
205
|
+
delete_and_wait(nsgs, nsg.name, nsg.resource_group, options)
|
206
|
+
end
|
206
207
|
end
|
207
208
|
end
|
208
209
|
end
|
@@ -243,13 +244,13 @@ module Azure
|
|
243
244
|
|
244
245
|
# In the unlikely event it did not unlock, just log and skip.
|
245
246
|
if disk.x_ms_lease_status.casecmp('unlocked') != 0
|
246
|
-
|
247
|
+
log('warn', "Unable to delete disk #{disk.container}/#{disk.name}")
|
247
248
|
return
|
248
249
|
end
|
249
250
|
end
|
250
251
|
|
251
252
|
storage_account.delete_blob(disk.container, disk.name, key)
|
252
|
-
|
253
|
+
log("Deleted blob #{disk.container}/#{disk.name}") if options[:verbose]
|
253
254
|
|
254
255
|
begin
|
255
256
|
status_file = File.basename(disk.name, '.vhd') + '.status'
|
@@ -257,7 +258,7 @@ module Azure
|
|
257
258
|
rescue Azure::Armrest::NotFoundException
|
258
259
|
# Ignore, does not always exist.
|
259
260
|
else
|
260
|
-
|
261
|
+
log("Deleted blob #{disk.container}/#{status_file}") if options[:verbose]
|
261
262
|
end
|
262
263
|
end
|
263
264
|
end
|
@@ -271,20 +272,15 @@ module Azure
|
|
271
272
|
def delete_and_wait(service, name, group, options)
|
272
273
|
resource_type = service.class.to_s.sub('Service', '').split('::').last
|
273
274
|
|
274
|
-
|
275
|
-
|
276
|
-
headers = service.delete(name, group)
|
275
|
+
log("Deleting #{resource_type} #{name}/#{group}") if options[:verbose]
|
277
276
|
|
278
|
-
|
279
|
-
status = wait(headers)
|
280
|
-
break if status.downcase.start_with?('succ') # Succeeded, Success, etc.
|
281
|
-
end
|
277
|
+
wait(service.delete(name, group), 0)
|
282
278
|
|
283
|
-
|
279
|
+
log("Deleted #{resource_type} #{name}/#{group}") if options[:verbose]
|
284
280
|
rescue Azure::Armrest::BadRequestException, Azure::Armrest::PreconditionFailedException => err
|
285
281
|
if options[:verbose]
|
286
282
|
msg = "Unable to delete #{resource_type} #{name}/#{group}, skipping. Message: #{err.message}"
|
287
|
-
|
283
|
+
log('warn', msg)
|
288
284
|
end
|
289
285
|
end
|
290
286
|
|
@@ -296,15 +292,6 @@ module Azure
|
|
296
292
|
rest_post(url)
|
297
293
|
nil
|
298
294
|
end
|
299
|
-
|
300
|
-
# Simple log messager. Use the Configuration.log if defined.
|
301
|
-
def log_message(msg, level = 'info')
|
302
|
-
if Azure::Armrest::Configuration.log
|
303
|
-
Azure::Armrest::Configuration.log.send(level.to_sym, msg)
|
304
|
-
else
|
305
|
-
warn msg
|
306
|
-
end
|
307
|
-
end
|
308
295
|
end
|
309
296
|
end
|
310
297
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: azure-armrest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel J. Berger
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2016-12-
|
14
|
+
date: 2016-12-16 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: json
|