docker-swarm-api 1.2.2 → 1.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1a8630271a889f20bc866426d01c86a9892e6fbe
4
- data.tar.gz: 41d722648cf609db8f83da1e27715cd7f519040f
3
+ metadata.gz: f231ed6bde66591fce70f5c3c5d653d3c21abd52
4
+ data.tar.gz: e33c69b6e3ad5a10a070d9de06f358093413780d
5
5
  SHA512:
6
- metadata.gz: 5c84719528dfc669814d3444647fb96b052da5a61be9480dbbb68992e6309e80ba1f34db7c7606c0144a3c8ff5fa5c76178676c35c20f81916a6c646373c6a6a
7
- data.tar.gz: 430e4921e366265643d67c917b77b4c97b7f8ce71d4216a352ea3fc2e5db6513f3f11a947d39c95e583e81072d4cdcf888bfbb5348353cef535314508241ed38
6
+ metadata.gz: 3885fddc8388316922b7a4371d1d10a64084a59fb8d373fd4e2a2df8fd76f241f510ce7bb4d098ed7427c95e2bf9f6702e52092b82372a6d9464a15abe033d52
7
+ data.tar.gz: 52a89eb4cede7dff51e836db236737711ceccd7e648d13ada96e18f50b5f91f7b18078194d10092306f3e21aa112d546389a81c581974bce888a1327efacbb29
data/README.md CHANGED
@@ -1,120 +1,85 @@
1
1
  # docker-swarm-api
2
2
 
3
- Must use Docker API Version of 1.24 or above.
3
+ Ruby GEM providing API for managing Docker Swarm clusters.
4
+
5
+ MIT License
4
6
 
5
- This project leverages swipely/docker-api, and adds Docker Swarm capability.
7
+ Must use Docker Engine Version of 1.12 or above. Docker Engine version 1.12.5 required to make overlay networks with API.
6
8
 
7
- Warning: cannot create overlay network in Docker Engine versions less than 1.13.
9
+ Must use Docker API Version of 1.24 or above.
10
+
11
+ This project leverages swipely/docker-api (https://github.com/swipely/docker-api), and adds Docker Swarm capability.
8
12
 
9
13
  Sample Usage
10
14
  ------------
11
15
  ```ruby
12
- # Create a Swarm cluster
16
+ # Make a connection to the Swarm manager's API. (Assumes port 2375 exposed for API)
13
17
  master_connection = Docker::Swarm::Connection.new('http://10.20.30.1:2375')
14
18
 
15
19
  # Manager node intializes swarm
16
- swarm_init_options = {
17
- "ListenAddr" => "0.0.0.0:2377",
18
- }
20
+ swarm_init_options = { "ListenAddr" => "0.0.0.0:2377" }
19
21
  swarm = Docker::Swarm::Swarm.init(swarm_init_options, master_connection)
20
22
 
21
- expect(swarm).to_not be nil
22
-
23
- nodes = Docker::Swarm::Node.all({}, master_connection)
23
+ # Gather all nodes available to swarm (overlay and bridges)
24
+ nodes = swarm.nodes()
24
25
  expect(nodes.length).to eq 1
25
26
 
26
27
  # Worker joins swarm
27
28
  worker_connection = Docker::Swarm::Connection.new('http://10.20.30.2:2375')
28
- swarm.join(worker_ip, worker_connection)
29
+ swarm.join_worker(worker_connection)
30
+
31
+ # Join another manager to the swarm
32
+ manager_2_connection = Docker::Swarm::Connection.new('http://10.20.30.3:2375')
33
+ swarm.join_manager(manager_2_connection)
29
34
 
30
35
  # Gather all nodes of swarm
31
- nodes = swarm.nodes
36
+ nodes = swarm.nodes()
32
37
 
33
38
  # Create a network which connect services
34
- network = swarm.create_network(network_name)
39
+ network = swarm.create_overlay_network(network_name)
35
40
 
36
41
  # Find all networks in swarm cluster
37
- networks = swarm.networks
42
+ networks = swarm.networks()
38
43
 
39
44
  # Create a service with 5 replicas
40
- service_create_options = {
41
- "Name" => "nginx",
42
- "TaskTemplate" => {
43
- "ContainerSpec" => {
44
- "Networks" => [network.id],
45
- "Image" => "nginx:1.11.7",
46
- "Mounts" => [
47
- ],
48
- "User" => "root"
49
- },
50
- "Env" => ["TEST_ENV=test"],
51
- "LogDriver" => {
52
- "Name" => "json-file",
53
- "Options" => {
54
- "max-file" => "3",
55
- "max-size" => "10M"
56
- }
57
- },
58
- "Placement" => {},
59
- "Resources" => {
60
- "Limits" => {
61
- "MemoryBytes" => 104857600
62
- },
63
- "Reservations" => {
64
- }
65
- },
66
- "RestartPolicy" => {
67
- "Condition" => "on-failure",
68
- "Delay" => 1,
69
- "MaxAttempts" => 3
70
- }
71
- },
72
- "Mode" => {
73
- "Replicated" => {
74
- "Replicas" => 5
75
- }
76
- },
77
- "UpdateConfig" => {
78
- "Delay" => 2,
79
- "Parallelism" => 2,
80
- "FailureAction" => "pause"
81
- },
82
- "EndpointSpec" => {
83
- "Ports" => [
84
- {
85
- "Protocol" => "tcp",
86
- "PublishedPort" => 80,
87
- "TargetPort" => 80
88
- }
89
- ]
90
- },
91
- "Labels" => {
92
- "foo" => "bar"
93
- }
94
- }
95
-
45
+ service_create_options = {"Name"=>"nginx",
46
+ "TaskTemplate" =>
47
+ {"ContainerSpec" =>
48
+ {"Networks" => [], "Image" => "nginx:1.11.7", "Mounts" => [], "User" => "root"},
49
+ "Env" => ["TEST_ENV=test"],
50
+ "LogDriver" => {"Name"=>"json-file", "Options"=>{"max-file"=>"3", "max-size"=>"10M"}},
51
+ "Placement" => {},
52
+ "Resources" => {"Limits"=>{"MemoryBytes"=>104857600}, "Reservations"=>{}},
53
+ "RestartPolicy" => {"Condition"=>"on-failure", "Delay"=>1, "MaxAttempts"=>3}},
54
+ "Mode"=>{"Replicated" => {"Replicas" => 5}},
55
+ "UpdateConfig" => {"Delay" => 2, "Parallelism" => 2, "FailureAction" => "pause"},
56
+ "EndpointSpec"=>
57
+ {"Ports" => [{"Protocol"=>"tcp", "PublishedPort" => 8181, "TargetPort" => 80}]},
58
+ "Labels" => {"layer" => "database"},
59
+ "Networks" => [{"Target" => "my-network"}]
60
+ }
96
61
  service = swarm.create_service(service_create_options)
97
62
 
98
63
  # Retrieve all manager nodes of swarm
99
- manager_nodes = swarm.manager_nodes
64
+ manager_nodes = swarm.manager_nodes()
100
65
 
101
66
  # Retrieve all worker nodes (that aren't managers)
102
- worker_nodes = swarm.worker_nodes
67
+ worker_nodes = swarm.worker_nodes()
103
68
 
104
69
  # Drain a worker node - stop hosting tasks/containers of services
105
70
  worker_node = worker_nodes.first
106
- worker_node.drain
71
+ worker_node.drain()
107
72
 
108
73
  # Gather all tasks (containers for service) being hosted by the swarm cluster
109
- tasks = swarm.tasks
74
+ tasks = swarm.tasks()
110
75
 
111
76
  # Scale up or down the number of replicas on a service
112
77
  service.scale(20)
113
78
 
114
79
  # Worker leaves the swarm - no forcing
115
- swarm.leave(false, worker_connection)
80
+ swarm.leave(worker_node, node)
116
81
 
117
- # Manager leaves the swarm - forced because manager's need to force the issue.
118
- swarm.leave(true, master_connection)
82
+ # Manager leaves the swarm - forced because last manager needs to use 'force' to leave the issue.
83
+ swarm.leave(manager_node, true)
119
84
 
120
85
  ```
@@ -20,13 +20,20 @@ class Docker::Swarm::Network
20
20
  return @hash['Name']
21
21
  end
22
22
 
23
+ def driver
24
+ return @hash['Driver']
25
+ end
26
+
23
27
  def remove
28
+ network_name = name
24
29
  response = @swarm.connection.delete("/networks/#{id()}", {}, expects: [200, 204, 500], full_response: true)
25
30
  if (response.status > 204)
26
31
  raise "Error deleting network (#{name}) HTTP-#{response.status} #{response.body}"
27
32
  end
33
+ while (@swarm.find_network_by_name(network_name) != nil)
34
+ sleep 1
35
+ end
28
36
  end
29
-
30
37
  end
31
38
 
32
39
  # EXAMPLE INSPECT OF OVERLAY NETWORK:
@@ -17,6 +17,12 @@ class Docker::Swarm::Service
17
17
  return @hash['ID']
18
18
  end
19
19
 
20
+ def reload()
21
+ s = @swarm.find_service(id())
22
+ @hash = s.hash
23
+ return self
24
+ end
25
+
20
26
  def network_ids
21
27
  network_ids = []
22
28
  @hash['Endpoint']['VirtualIPs'].each do |network_info|
@@ -41,6 +47,65 @@ class Docker::Swarm::Service
41
47
  self.update(@hash['Spec'])
42
48
  end
43
49
 
50
+
51
+ def self.DEFAULT_OPTIONS
52
+ default_service_create_options = {
53
+ "Name" => "<<Required>>",
54
+ "TaskTemplate" => {
55
+ "ContainerSpec" => {
56
+ "Image" => "<<Required>>",
57
+ "Mounts" => [],
58
+ "User" => "root"
59
+ },
60
+ "Env" => [],
61
+ "LogDriver" => {
62
+ "Name" => "json-file",
63
+ "Options" => {
64
+ "max-file" => "3",
65
+ "max-size" => "10M"
66
+ }
67
+ },
68
+ "Placement" => {},
69
+ "Resources" => {
70
+ "Limits" => {
71
+ "MemoryBytes" => 104857600
72
+ },
73
+ "Reservations" => {
74
+ # "NanoCPUs" => ?
75
+ # MemoryBytes =>
76
+ }
77
+ },
78
+ "RestartPolicy" => {
79
+ "Condition" => "on-failure",
80
+ "Delay" => 1,
81
+ "MaxAttempts" => 3
82
+ }
83
+ }, # End of TaskTemplate
84
+ "Mode" => {
85
+ "Replicated" => {
86
+ "Replicas" => 1
87
+ }
88
+ },
89
+ "UpdateConfig" => {
90
+ "Delay" => 2,
91
+ "Parallelism" => 2,
92
+ "FailureAction" => "pause"
93
+ },
94
+ "EndpointSpec" => {
95
+ "Ports" => [
96
+ {
97
+ # "Protocol" => "http",
98
+ # "PublishedPort" => 2881,
99
+ # "TargetPort" => 2881
100
+ }
101
+ ]
102
+ },
103
+ "Labels" => {
104
+ "foo" => "bar"
105
+ }
106
+ }
107
+ return default_service_create_options
108
+ end
44
109
 
45
110
 
46
111
  end
@@ -9,61 +9,58 @@ class Docker::Swarm::Swarm
9
9
 
10
10
  def initialize(hash, manager_connection, options = {})
11
11
  @hash = hash
12
- # @manager_connection = manager_connection
13
12
  @id = hash['ID']
14
13
  @worker_join_token = hash['JoinTokens']['Worker']
15
14
  @manager_join_token = hash['JoinTokens']['Manager']
16
15
  @node_hash = {}
17
16
  @manager_connection = manager_connection
18
- nodes.each do |node|
19
- node_connection = nil
20
- docker_port = options[:docker_api_port] || 2375
21
- if (node.hash['ManagerStatus'])
22
- ip_address = node.hash['ManagerStatus']['Addr'].split(":").first
23
- manager_ip_address = @manager_connection.url.split('//').last.split(':').first
24
- if (ip_address == manager_ip_address)
25
- node.connection = @manager_connection
26
- else
27
- node.connection = Docker::Swarm::Connection.new("tcp://#{ip_address}:#{docker_port}")
28
- end
29
- else
30
- ip_address = nil
31
- begin
32
- ip_address = Resolv::DNS.new.getaddress(node.host_name())
33
- rescue
34
- ip_address = Resolv::Hosts.new.getaddress(node.host_name())
35
- if (!ip_address)
36
- host_addresses = options[:host_addresses]
37
- ip_address = host_addresses[node.host_name]
38
- end
39
- end
40
- node.connection = Docker::Swarm::Connection.new("tcp://#{ip_address}:#{docker_port}")
41
- end
42
- @node_hash[node.id] = {hash: node.hash, connection: node.connection}
43
-
44
- end
17
+ end
18
+
19
+ def store_manager(manager_connection, listen_address_and_port)
20
+ node = nodes.find {|n|
21
+ (n.hash['ManagerStatus']) && (n.hash['ManagerStatus']['Leader'] == true) && (n.hash['ManagerStatus']['Addr'] == listen_address_and_port)
22
+ }
23
+ raise "Node not found for: #{listen_address}" if (!node)
24
+ @node_hash[node.id] = {hash: node.hash, connection: manager_connection}
45
25
  end
46
26
 
47
- def join(node_connection, join_token)
27
+ def join(node_connection, join_token = nil, listen_address = "0.0.0.0:2377")
28
+ join_token = @worker_join_token
48
29
  node_ids_before = nodes().collect {|n| n.id}
49
30
  query = {}
50
31
  master_ip = self.connection.url.split("//").last.split(":").first
51
32
  new_node_ip = node_connection.url.split("//").last.split(":").first
33
+
52
34
  join_options = {
53
- "ListenAddr" => "0.0.0.0:2377",
35
+ "ListenAddr" => "#{listen_address}",
54
36
  "AdvertiseAddr" => "#{new_node_ip}:2377",
55
37
  "RemoteAddrs" => ["#{master_ip}:2377"],
56
38
  "JoinToken" => join_token
57
39
  }
58
40
  new_node = nil
59
- resp = node_connection.post('/swarm/join', query, :body => join_options.to_json, expects: [200])
60
- nodes.each do |node|
61
- if (!node_ids_before.include? node.id)
62
- new_node = node
63
- @node_hash[node.id] = {hash: node.hash, connection: node_connection}
41
+ response = node_connection.post('/swarm/join', query, :body => join_options.to_json, expects: [200, 406, 500], full_response: true)
42
+ if (response.status == 200)
43
+ nodes.each do |node|
44
+ if (!node_ids_before.include? node.id)
45
+ new_node = node
46
+ @node_hash[node.id] = {hash: node.hash, connection: node_connection}
47
+ end
64
48
  end
49
+ return new_node
50
+ elsif (response.status == 406)
51
+ puts "Node is already part of a swarm - maybe this swarm, maybe another swarm."
52
+ return nil
53
+ else
54
+ raise "Error joining (#{node_connection}): HTTP-#{response.status} #{response.body}"
65
55
  end
66
- return new_node
56
+ end
57
+
58
+ def join_worker(node_connection, listen_address = "0.0.0.0:2377")
59
+ join(node_connection, @worker_join_token)
60
+ end
61
+
62
+ def join_manager(manager_connection, listen_address = "0.0.0.0:2377")
63
+ join(node_connection, @manager_join_token, listen_address)
67
64
  end
68
65
 
69
66
  def connection
@@ -76,14 +73,6 @@ class Docker::Swarm::Swarm
76
73
  return @manager_connection
77
74
  end
78
75
 
79
- def join_worker(node_connection)
80
- join(node_connection, @worker_join_token)
81
- end
82
-
83
- def join_manager(manager_connection)
84
- join(node_connection, @manager_join_token)
85
- end
86
-
87
76
  def remove
88
77
  services().each do |service|
89
78
  service.remove()
@@ -144,46 +133,54 @@ class Docker::Swarm::Swarm
144
133
  return all_networks
145
134
  end
146
135
 
147
- def create_network(network_name, options = {})
148
- defaults = {
136
+ def create_network(options)
137
+ response = connection.post('/networks/create', {}, body: options.to_json, expects: [200, 201, 500], full_response: true)
138
+ if (response.status <= 201)
139
+ hash = JSON.parse(response.body)
140
+ response = connection.get("/networks/#{hash['Id']}", {}, expects: [200, 201], full_response: true)
141
+ hash = Docker::Util.parse_json(response.body)
142
+ network = Docker::Swarm::Network.new(self, hash)
143
+ return network
144
+ else
145
+ raise "Error creating network: HTTP-#{response.status} - #{response.body}"
146
+ end
147
+ end
148
+
149
+ def create_network_overlay(network_name)
150
+ max_vxlanid = 200
151
+ networks.each do |network|
152
+ if (network.driver == 'overlay')
153
+ if (network.hash['Options'])
154
+ vxlanid = network.hash['Options']["com.docker.network.driver.overlay.vxlanid_list"]
155
+ if (vxlanid) && (vxlanid.to_i > max_vxlanid)
156
+ max_vxlanid = vxlanid.to_i
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ options = {
149
163
  "Name" => network_name,
150
164
  "CheckDuplicate" => true,
151
- # "Driver" => "bridge",
152
165
  "Driver" => "overlay",
153
166
  "EnableIPv6" => false,
154
- # "IPAM" => {
155
- # "Driver" => "default",
156
- # "Config" => [
157
- # {
158
- # "Subnet" => "172.20.0.0/16",
159
- # "IPRange" => "172.20.10.0/24",
160
- # "Gateway" => "172.20.10.11"
161
- # }
162
- # ],
163
- # "Options" => {
164
- # # "foo" => "bar"
165
- # }
166
- # },
167
+ "IPAM" => {
168
+ "Driver" => "default",
169
+ "Config" => [
170
+ ],
171
+ "Options" => {
172
+ }
173
+ },
167
174
  "Internal" => false,
168
175
  "Options" => {
169
- "com.docker.network.driver.overlay.vxlanid_list" => "257"
176
+ "com.docker.network.driver.overlay.vxlanid_list" => (max_vxlanid + 1).to_s
170
177
  },
171
178
  "Labels" => {
172
179
  # "com.example.some-label": "some-value",
173
180
  # "com.example.some-other-label": "some-other-value"
174
181
  }
175
182
  }
176
- opts = defaults.merge(options)
177
- response = connection.post('/networks/create', {}, body: opts.to_json, expects: [200, 201, 500], full_response: true)
178
- if (response.status <= 201)
179
- hash = JSON.parse(response.body)
180
- response = connection.get("/networks/#{hash['Id']}", {}, expects: [200, 201], full_response: true)
181
- hash = Docker::Util.parse_json(response.body)
182
- network = Docker::Swarm::Network.new(self, hash)
183
- return network
184
- else
185
- raise "Error creating network: HTTP-#{response.status} - #{response.body}"
186
- end
183
+ create_network(options)
187
184
  end
188
185
 
189
186
  def find_network_by_name(network_name)
@@ -194,7 +191,7 @@ class Docker::Swarm::Swarm
194
191
  end
195
192
  return nil
196
193
  end
197
-
194
+
198
195
  # Return all of the Nodes.
199
196
  def nodes
200
197
  opts = {}
@@ -215,7 +212,7 @@ class Docker::Swarm::Swarm
215
212
 
216
213
  def create_service(opts = {})
217
214
  query = {}
218
- response = self.connection.post('/services/create', query, :body => opts.to_json, expects: [201, 500], full_response: true)
215
+ response = self.connection.post('/services/create', query, :body => opts.to_json, expects: [201, 404, 409, 500], full_response: true)
219
216
  if (response.status <= 201)
220
217
  info = JSON.parse(response.body)
221
218
  service_id = info['ID']
@@ -234,7 +231,7 @@ class Docker::Swarm::Swarm
234
231
  return Docker::Swarm::Service.new(self, hash)
235
232
  end
236
233
 
237
- def find_service_with_name(name)
234
+ def find_service_by_name(name)
238
235
  services.each do |service|
239
236
  return service if (service.name == name)
240
237
  end
@@ -253,23 +250,28 @@ class Docker::Swarm::Swarm
253
250
  return items
254
251
  end
255
252
 
256
- def discover_nodes
257
- # {discover_nodes: true, worker_docker_port: 2375}
258
-
259
- end
260
-
261
253
  # Initialize Swarm
262
254
  def self.init(opts, connection)
263
255
  query = {}
264
- resp = connection.post('/swarm/init', query, :body => opts.to_json, full_response: true)
265
- return Docker::Swarm::Swarm.swarm(opts, connection)
256
+ resp = connection.post('/swarm/init', query, :body => opts.to_json, full_response: true, expects: [200, 404, 500])
257
+ if (resp.status == 200)
258
+ swarm = Docker::Swarm::Swarm.swarm(opts, connection)
259
+ manager_node = swarm.nodes.find {|n|
260
+ (n.hash['ManagerStatus']) && (n.hash['ManagerStatus']['Leader'] == true)
261
+ }
262
+ listen_address = manager_node.hash['ManagerStatus']['Addr']
263
+ swarm.store_manager(connection, listen_address)
264
+ return swarm
265
+ else
266
+ raise "Bad response: #{resp.status} #{resp.body}"
267
+ end
266
268
  end
267
269
 
268
270
  # docker swarm join-token -q worker
269
271
  def self.swarm(opts, connection)
270
272
  query = {}
271
- resp = connection.get('/swarm', query, :body => opts.to_json, expects: [200, 406], full_response: true)
272
- if (resp.status == 406)
273
+ resp = connection.get('/swarm', query, :body => opts.to_json, expects: [200, 404, 406], full_response: true)
274
+ if (resp.status == 406) || (resp.status == 404)
273
275
  return nil
274
276
  elsif (resp.status == 200)
275
277
  hash = JSON.parse(resp.body)
@@ -290,11 +292,16 @@ class Docker::Swarm::Swarm
290
292
 
291
293
  def self.find(connection, options = {})
292
294
  query = {}
293
- response = connection.get('/swarm', query, expects: [200, 406], full_response: true)
295
+ response = connection.get('/swarm', query, expects: [200, 404, 406], full_response: true)
294
296
  if (response.status == 200)
295
297
  swarm = Docker::Swarm::Swarm.new(JSON.parse(response.body), connection, options)
298
+ manager_node = swarm.nodes.find {|n|
299
+ (n.hash['ManagerStatus']) && (n.hash['ManagerStatus']['Leader'] == true)
300
+ }
301
+ listen_address = manager_node.hash['ManagerStatus']['Addr']
302
+ swarm.store_manager(connection, listen_address)
296
303
  return swarm
297
- elsif (response.status == 406)
304
+ elsif (response.status > 200)
298
305
  return nil
299
306
  else
300
307
  raise "Error finding swarm: HTTP-#{response.status} #{response.body}"
@@ -19,6 +19,12 @@ class Docker::Swarm::Task
19
19
  def service_id
20
20
  @hash['ServiceID']
21
21
  end
22
+
23
+ def service
24
+ return @swarm.services.find { |service|
25
+ self.service_id == service.id
26
+ }
27
+ end
22
28
 
23
29
  def node_id
24
30
  @hash['NodeID']
@@ -36,5 +42,15 @@ class Docker::Swarm::Task
36
42
  @hash['Status']['State'].to_sym
37
43
  end
38
44
 
45
+ def networks
46
+ all_networks = @swarm.networks
47
+ nets = []
48
+ self.hash['NetworksAttachments'].each do |net_hash|
49
+ hash = net_hash['Network']
50
+ network_id = hash['ID']
51
+ nets << all_networks.find {|net| net.id == network_id}
52
+ end
53
+ return nets
54
+ end
39
55
 
40
56
  end
@@ -1,7 +1,7 @@
1
1
  module Docker
2
2
  module Swarm
3
3
  # The version of the docker-api gem.
4
- VERSION = '1.2.2'
4
+ VERSION = '1.2.3'
5
5
 
6
6
  # The version of the compatible Docker remote API.
7
7
  API_VERSION = '1.24'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docker-swarm-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Moore / Rogue Wave Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-28 00:00:00.000000000 Z
11
+ date: 2017-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json