docker-swarm-sdk 1.2.5
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +103 -0
- data/lib/docker-swarm-sdk.rb +1 -0
- data/lib/docker-swarm.rb +139 -0
- data/lib/docker/swarm/connection.rb +21 -0
- data/lib/docker/swarm/network.rb +68 -0
- data/lib/docker/swarm/node.rb +191 -0
- data/lib/docker/swarm/service.rb +121 -0
- data/lib/docker/swarm/swarm.rb +374 -0
- data/lib/docker/swarm/task.rb +64 -0
- data/lib/docker/swarm/version.rb +9 -0
- metadata +209 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 166f2d37a054fea0b51d90d32f48cdace6ccd133
|
4
|
+
data.tar.gz: 990589c2d8478c55dcb45ca6d868f034ceeb94f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4bafe8fd12192bb44d9d1074f08b385d1c38cf93583f5fed4f698021e1d1c3096d1ea5e262242173cf2ee2f0e09aef61345f2123aec78591f8b79008c22369ec
|
7
|
+
data.tar.gz: 271c2ea0d98e7a9ebe679b94f79f623bc691b08bb80e390949852ce834523d0f9f283bbb09acc7d51d5b563f27766cd37b8384e5e5899f95521c113b484b5b22
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Mike Moore
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# docker-swarm-api
|
2
|
+
|
3
|
+
Ruby GEM providing API for managing Docker Swarm clusters.
|
4
|
+
|
5
|
+
MIT License
|
6
|
+
|
7
|
+
Must use Docker Engine Version of 1.12 or above. Docker Engine version 1.12.5 required to make overlay networks with API.
|
8
|
+
|
9
|
+
Must use Docker API Version of 1.24 or above.
|
10
|
+
|
11
|
+
|
12
|
+
Docker Swarm is improving rapidly. The controls for services has seen great improvements lately. This GEM helps connect your Ruby scripts/applications to create and extend your swarm and then manage services upon the swarm.
|
13
|
+
|
14
|
+
This project leverages swipely/docker-api (https://github.com/swipely/docker-api), and adds Docker Swarm capability.
|
15
|
+
|
16
|
+
Sample Usage
|
17
|
+
------------
|
18
|
+
```ruby
|
19
|
+
# Make a connection to the Swarm manager's API. (Assumes port 2375 exposed for API)
|
20
|
+
master_connection = Docker::Swarm::Connection.new('http://10.20.30.1:2375')
|
21
|
+
|
22
|
+
# If swarm on the swarm master and using socket:
|
23
|
+
master_connection = Docker::Swarm::Connection.new('unix:///var/run/docker.sock')
|
24
|
+
|
25
|
+
# Manager node intializes swarm
|
26
|
+
swarm_init_options = { "ListenAddr" => "0.0.0.0:2377" }
|
27
|
+
swarm = Docker::Swarm::Swarm.init(swarm_init_options, master_connection)
|
28
|
+
|
29
|
+
# Gather all nodes available to swarm (overlay and bridges)
|
30
|
+
nodes = swarm.nodes()
|
31
|
+
expect(nodes.length).to eq 1
|
32
|
+
|
33
|
+
# Worker joins swarm
|
34
|
+
worker_connection = Docker::Swarm::Connection.new('http://10.20.30.2:2375')
|
35
|
+
swarm.join_worker(worker_connection)
|
36
|
+
|
37
|
+
# Worker joins without master api connection
|
38
|
+
swarm_options = { "manager_ip" => "10.20.30.1", "node_ip" => "10.20.30.2", "JoinTokens" => {"Worker" => "FooBar" }}
|
39
|
+
swarm = Docker::Swarm::Swarm.new(swarm_options)
|
40
|
+
local_connection = Docker::Swarm::Connection.new('unix:///var/run/docker.sock')
|
41
|
+
swarm.join_worker(local_connection)
|
42
|
+
|
43
|
+
# Join another manager to the swarm
|
44
|
+
manager_2_connection = Docker::Swarm::Connection.new('http://10.20.30.3:2375')
|
45
|
+
swarm.join_manager(manager_2_connection)
|
46
|
+
|
47
|
+
# Manager joins without master api connection
|
48
|
+
swarm_options = { "manager_ip" => "10.20.30.1", "node_ip" => "10.20.30.2", "JoinTokens" => {"Master" => "FooBar" }}
|
49
|
+
swarm = Docker::Swarm::Swarm.new(swarm_options)
|
50
|
+
local_connection = Docker::Swarm::Connection.new('unix:///var/run/docker.sock')
|
51
|
+
swarm.join_manager(local_connection)
|
52
|
+
|
53
|
+
# Gather all nodes of swarm
|
54
|
+
nodes = swarm.nodes()
|
55
|
+
|
56
|
+
# Create a network which connect services
|
57
|
+
network = swarm.create_overlay_network(network_name)
|
58
|
+
|
59
|
+
# Find all networks in swarm cluster
|
60
|
+
networks = swarm.networks()
|
61
|
+
|
62
|
+
# Create a service with 5 replicas
|
63
|
+
service_create_options = {"Name"=>"nginx",
|
64
|
+
"TaskTemplate" =>
|
65
|
+
{"ContainerSpec" =>
|
66
|
+
{"Networks" => [], "Image" => "nginx:1.11.7", "Mounts" => [], "User" => "root"},
|
67
|
+
"Env" => ["TEST_ENV=test"],
|
68
|
+
"LogDriver" => {"Name"=>"json-file", "Options"=>{"max-file"=>"3", "max-size"=>"10M"}},
|
69
|
+
"Placement" => {},
|
70
|
+
"Resources" => {"Limits"=>{"MemoryBytes"=>104857600}, "Reservations"=>{}},
|
71
|
+
"RestartPolicy" => {"Condition"=>"on-failure", "Delay"=>1, "MaxAttempts"=>3}},
|
72
|
+
"Mode"=>{"Replicated" => {"Replicas" => 5}},
|
73
|
+
"UpdateConfig" => {"Delay" => 2, "Parallelism" => 2, "FailureAction" => "pause"},
|
74
|
+
"EndpointSpec"=>
|
75
|
+
{"Ports" => [{"Protocol"=>"tcp", "PublishedPort" => 8181, "TargetPort" => 80}]},
|
76
|
+
"Labels" => {"layer" => "database"},
|
77
|
+
"Networks" => [{"Target" => "my-network"}]
|
78
|
+
}
|
79
|
+
service = swarm.create_service(service_create_options)
|
80
|
+
|
81
|
+
# Retrieve all manager nodes of swarm
|
82
|
+
manager_nodes = swarm.manager_nodes()
|
83
|
+
|
84
|
+
# Retrieve all worker nodes (that aren't managers)
|
85
|
+
worker_nodes = swarm.worker_nodes()
|
86
|
+
|
87
|
+
# Drain a worker node - stop hosting tasks/containers of services
|
88
|
+
worker_node = worker_nodes.first
|
89
|
+
worker_node.drain()
|
90
|
+
|
91
|
+
# Gather all tasks (containers for service) being hosted by the swarm cluster
|
92
|
+
tasks = swarm.tasks()
|
93
|
+
|
94
|
+
# Scale up or down the number of replicas on a service
|
95
|
+
service.scale(20)
|
96
|
+
|
97
|
+
# Worker leaves the swarm - no forcing
|
98
|
+
swarm.leave(worker_node, node)
|
99
|
+
|
100
|
+
# Manager leaves the swarm - forced because last manager needs to use 'force' to leave the issue.
|
101
|
+
swarm.leave(manager_node, true)
|
102
|
+
|
103
|
+
```
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative './docker-swarm'
|
data/lib/docker-swarm.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'json'
|
3
|
+
require 'excon'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'base64'
|
6
|
+
require 'find'
|
7
|
+
require 'rubygems/package'
|
8
|
+
require 'uri'
|
9
|
+
require 'open-uri'
|
10
|
+
|
11
|
+
# Add the Hijack middleware at the top of the middleware stack so it can
|
12
|
+
# potentially hijack HTTP sockets (when attaching to stdin) before other
|
13
|
+
# middlewares try and parse the response.
|
14
|
+
require 'excon/middlewares/hijack'
|
15
|
+
Excon.defaults[:middlewares].unshift Excon::Middleware::Hijack
|
16
|
+
|
17
|
+
Excon.defaults[:middlewares] << Excon::Middleware::RedirectFollower
|
18
|
+
|
19
|
+
# The top-level module for this gem. Its purpose is to hold global
|
20
|
+
# configuration variables that are used as defaults in other classes.
|
21
|
+
module Docker
|
22
|
+
module Swarm
|
23
|
+
attr_accessor :creds, :logger
|
24
|
+
|
25
|
+
require_relative './docker/swarm/node'
|
26
|
+
require_relative './docker/swarm/service'
|
27
|
+
require_relative './docker/swarm/swarm'
|
28
|
+
require_relative './docker/swarm/connection'
|
29
|
+
require_relative './docker/swarm/network'
|
30
|
+
require_relative './docker/swarm/task'
|
31
|
+
|
32
|
+
def default_socket_url
|
33
|
+
'unix:///var/run/docker.sock'
|
34
|
+
end
|
35
|
+
|
36
|
+
def env_url
|
37
|
+
ENV['DOCKER_URL'] || ENV['DOCKER_HOST']
|
38
|
+
end
|
39
|
+
|
40
|
+
def env_options
|
41
|
+
if cert_path = ENV['DOCKER_CERT_PATH']
|
42
|
+
{
|
43
|
+
client_cert: File.join(cert_path, 'cert.pem'),
|
44
|
+
client_key: File.join(cert_path, 'key.pem'),
|
45
|
+
ssl_ca_file: File.join(cert_path, 'ca.pem'),
|
46
|
+
scheme: 'https'
|
47
|
+
}.merge(ssl_options)
|
48
|
+
else
|
49
|
+
{}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def ssl_options
|
54
|
+
if ENV['DOCKER_SSL_VERIFY'] == 'false'
|
55
|
+
{
|
56
|
+
ssl_verify_peer: false
|
57
|
+
}
|
58
|
+
else
|
59
|
+
{}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def url
|
64
|
+
@url ||= env_url || default_socket_url
|
65
|
+
# docker uses a default notation tcp:// which means tcp://localhost:2375
|
66
|
+
if @url == 'tcp://'
|
67
|
+
@url = 'tcp://localhost:2375'
|
68
|
+
end
|
69
|
+
@url
|
70
|
+
end
|
71
|
+
|
72
|
+
def options
|
73
|
+
@options ||= env_options
|
74
|
+
end
|
75
|
+
|
76
|
+
def url=(new_url)
|
77
|
+
@url = new_url
|
78
|
+
reset_connection!
|
79
|
+
end
|
80
|
+
|
81
|
+
def options=(new_options)
|
82
|
+
@options = env_options.merge(new_options || {})
|
83
|
+
reset_connection!
|
84
|
+
end
|
85
|
+
|
86
|
+
def connection
|
87
|
+
@connection ||= Connection.new(url, options)
|
88
|
+
end
|
89
|
+
|
90
|
+
def reset!
|
91
|
+
@url = nil
|
92
|
+
@options = nil
|
93
|
+
reset_connection!
|
94
|
+
end
|
95
|
+
|
96
|
+
def reset_connection!
|
97
|
+
@connection = nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get the version of Go, Docker, and optionally the Git commit.
|
101
|
+
def version(connection = self.connection)
|
102
|
+
Util.parse_json(connection.get('/version'))
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get more information about the Docker server.
|
106
|
+
def info(connection = self.connection)
|
107
|
+
Util.parse_json(connection.get('/info'))
|
108
|
+
end
|
109
|
+
|
110
|
+
# Ping the Docker server.
|
111
|
+
def ping(connection = self.connection)
|
112
|
+
connection.get('/_ping')
|
113
|
+
end
|
114
|
+
|
115
|
+
# Login to the Docker registry.
|
116
|
+
def authenticate!(options = {}, connection = self.connection)
|
117
|
+
creds = options.to_json
|
118
|
+
connection.post('/auth', {}, :body => creds)
|
119
|
+
@creds = creds
|
120
|
+
true
|
121
|
+
rescue Docker::Error::ServerError, Docker::Error::UnauthorizedError
|
122
|
+
raise Docker::Error::AuthenticationError
|
123
|
+
end
|
124
|
+
|
125
|
+
# When the correct version of Docker is installed, returns true. Otherwise,
|
126
|
+
# raises a VersionError.
|
127
|
+
def validate_version!
|
128
|
+
Docker.info
|
129
|
+
true
|
130
|
+
rescue Docker::Error::DockerError
|
131
|
+
raise Docker::Error::VersionError, "Expected API Version: #{API_VERSION}"
|
132
|
+
end
|
133
|
+
|
134
|
+
module_function :default_socket_url, :env_url, :url, :url=, :env_options,
|
135
|
+
:options, :options=, :creds, :creds=, :logger, :logger=,
|
136
|
+
:connection, :reset!, :reset_connection!, :version, :info,
|
137
|
+
:ping, :authenticate!, :validate_version!, :ssl_options
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This class represents a Connection to a Docker server. The Connection is
|
2
|
+
# immutable in that once the url and options is set they cannot be changed.
|
3
|
+
class Docker::Swarm::Connection < Docker::Connection
|
4
|
+
|
5
|
+
def initialize(url, opts = {})
|
6
|
+
super(url, opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
# Send a request to the server with the `
|
11
|
+
def request(*args, &block)
|
12
|
+
request = compile_request_params(*args, &block)
|
13
|
+
log_request(request)
|
14
|
+
if (args.last[:full_response] == true)
|
15
|
+
resource.request(request)
|
16
|
+
else
|
17
|
+
resource.request(request).body
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'docker-api'
|
2
|
+
|
3
|
+
class Docker::Swarm::Network
|
4
|
+
attr_reader :hash
|
5
|
+
|
6
|
+
def initialize(swarm, hash)
|
7
|
+
@hash = hash
|
8
|
+
@swarm = swarm
|
9
|
+
end
|
10
|
+
|
11
|
+
def connection
|
12
|
+
return @swarm.connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def id
|
16
|
+
return @hash['Id']
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
return @hash['Name']
|
21
|
+
end
|
22
|
+
|
23
|
+
def driver
|
24
|
+
return @hash['Driver']
|
25
|
+
end
|
26
|
+
|
27
|
+
def subnets
|
28
|
+
if (@hash['IPAM']) && (@hash['IPAM']['Config'])
|
29
|
+
return @hash['IPAM']['Config']
|
30
|
+
end
|
31
|
+
return []
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove
|
35
|
+
if (@swarm)
|
36
|
+
@swarm.nodes.each do |node|
|
37
|
+
node.remove_network(self)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
# EXAMPLE INSPECT OF OVERLAY NETWORK:
|
45
|
+
# {
|
46
|
+
# "Name": "overlay1",
|
47
|
+
# "Id": "3eluvldbrv17xw6w39xxgg30a",
|
48
|
+
# "Scope": "swarm",
|
49
|
+
# "Driver": "overlay",
|
50
|
+
# "EnableIPv6": false,
|
51
|
+
# "IPAM": {
|
52
|
+
# "Driver": "default",
|
53
|
+
# "Options": null,
|
54
|
+
# "Config": [
|
55
|
+
# {
|
56
|
+
# "Subnet": "10.0.9.0/24",
|
57
|
+
# "Gateway": "10.0.9.1"
|
58
|
+
# }
|
59
|
+
# ]
|
60
|
+
# },
|
61
|
+
# "Internal": false,
|
62
|
+
# "Containers": null,
|
63
|
+
# "Options": {
|
64
|
+
# "com.docker.network.driver.overlay.vxlanid_list": "257"
|
65
|
+
# },
|
66
|
+
# "Labels": null
|
67
|
+
# }
|
68
|
+
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# This class represents a Docker Swarm Node.
|
2
|
+
class Docker::Swarm::Node
|
3
|
+
attr_reader :hash, :swarm
|
4
|
+
AVAILABILITY = {
|
5
|
+
active: "active",
|
6
|
+
drain: "drain"
|
7
|
+
}
|
8
|
+
|
9
|
+
def initialize(swarm, hash)
|
10
|
+
@hash = hash
|
11
|
+
@swarm = swarm
|
12
|
+
end
|
13
|
+
|
14
|
+
def refresh
|
15
|
+
query = {}
|
16
|
+
response = @swarm.connection.get("/nodes/#{id}", query, expects: [200])
|
17
|
+
@hash = JSON.parse(response)
|
18
|
+
end
|
19
|
+
|
20
|
+
def id
|
21
|
+
return @hash['ID']
|
22
|
+
end
|
23
|
+
|
24
|
+
def host_name
|
25
|
+
return @hash['Description']['Hostname']
|
26
|
+
end
|
27
|
+
|
28
|
+
def connection
|
29
|
+
if (@swarm) && (@swarm.node_hash[id()])
|
30
|
+
return @swarm.node_hash[id()][:connection]
|
31
|
+
else
|
32
|
+
return nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def role
|
37
|
+
if (@hash['Spec']['Role'] == "worker")
|
38
|
+
return :worker
|
39
|
+
elsif (@hash['Spec']['Role'] == "manager")
|
40
|
+
return :manager
|
41
|
+
else
|
42
|
+
raise "Couldn't determine machine role from spec: #{@hash['Spec']}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def availability
|
47
|
+
return @hash['Spec']['Availability'].to_sym
|
48
|
+
end
|
49
|
+
|
50
|
+
def status
|
51
|
+
return @hash['Status']['State']
|
52
|
+
end
|
53
|
+
|
54
|
+
def drain(opts = {})
|
55
|
+
change_availability(:drain)
|
56
|
+
if (opts[:wait_for_drain])
|
57
|
+
opts[:wait_seconds]
|
58
|
+
while (running_tasks.length > 0)
|
59
|
+
puts "Waiting for node (#{host_name}) to drain. Still has #{running_tasks.length} tasks running."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def swarm_connection
|
65
|
+
node_hash = @swarm.node_hash[self.id]
|
66
|
+
if (node_hash)
|
67
|
+
return node_hash[:connection]
|
68
|
+
end
|
69
|
+
return nil
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def running_tasks
|
74
|
+
return tasks.select {|t| t.status == 'running'}
|
75
|
+
end
|
76
|
+
|
77
|
+
def tasks
|
78
|
+
return @swarm.tasks.select {|t|
|
79
|
+
(t.node != nil) && (t.node.id == self.id)
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def activate
|
84
|
+
change_availability(:active)
|
85
|
+
end
|
86
|
+
|
87
|
+
def remove
|
88
|
+
leave(true)
|
89
|
+
refresh
|
90
|
+
start_time = Time.now
|
91
|
+
while (self.status != 'down')
|
92
|
+
refresh
|
93
|
+
raise "Node not down 60 seconds after leaving swarm: #{self.host_name}" if (Time.now.to_i - start_time.to_i > 60)
|
94
|
+
end
|
95
|
+
Docker::Swarm::Node.remove(self.id, @swarm.connection)
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
def remove_network_with_name(network_name)
|
100
|
+
network = find_network_by_name(network_name)
|
101
|
+
self.remove_network(network) if (network)
|
102
|
+
end
|
103
|
+
|
104
|
+
def remove_network(network)
|
105
|
+
attempts = 0
|
106
|
+
if (self.connection == nil)
|
107
|
+
puts "Warning: node asked to remove network, but no connection for node: #{self.id} #{self.host_name}"
|
108
|
+
else
|
109
|
+
while (self.find_network_by_id(network.id) != nil)
|
110
|
+
response = self.connection.delete("/networks/#{network.id}", {}, expects: [204, 404, 500], full_response: true)
|
111
|
+
if (response.status == 500)
|
112
|
+
puts "Warning: Deleting network (#{network.name}) from #{self.host_name} returned HTTP-#{response.status} #{response.body}"
|
113
|
+
end
|
114
|
+
|
115
|
+
sleep 1
|
116
|
+
attempts += 1
|
117
|
+
if (attempts > 30)
|
118
|
+
raise "Failed to remove network: #{network.name} from #{self.host_name}, operation timed out. Response: HTTP#{response.status} #{response.body}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def leave(force = true)
|
126
|
+
drain(wait_for_drain: true, wait_seconds: 60)
|
127
|
+
# change_availability(:active)
|
128
|
+
@swarm.leave(self, force)
|
129
|
+
end
|
130
|
+
|
131
|
+
def change_availability(new_availability)
|
132
|
+
raise "Bad availability param: #{availability}" if (!AVAILABILITY[availability])
|
133
|
+
refresh
|
134
|
+
if (self.availability != new_availability)
|
135
|
+
@hash['Spec']['Availability'] = AVAILABILITY[new_availability]
|
136
|
+
query = {version: @hash['Version']['Index']}
|
137
|
+
response = @swarm.connection.post("/nodes/#{self.id}/update", query, :body => @hash['Spec'].to_json, expects: [200, 500], full_response: true)
|
138
|
+
if (response.status != 200)
|
139
|
+
raise "Error changing node availability: #{response.body} HTTP-#{response.status}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def networks()
|
145
|
+
if (connection)
|
146
|
+
return Docker::Swarm::Node.networks_on_host(connection, @swarm)
|
147
|
+
else
|
148
|
+
debugger
|
149
|
+
raise "No connection set for node: #{self.host_name}, ID: #{self.id}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def find_network_by_name(network_name)
|
154
|
+
networks.each do |network|
|
155
|
+
if (network.name == network_name)
|
156
|
+
return network
|
157
|
+
end
|
158
|
+
end
|
159
|
+
return nil
|
160
|
+
end
|
161
|
+
|
162
|
+
def find_network_by_id(network_id)
|
163
|
+
networks.each do |network|
|
164
|
+
if (network.id == network_id)
|
165
|
+
return network
|
166
|
+
end
|
167
|
+
end
|
168
|
+
return nil
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
def self.remove(node_id, connection)
|
173
|
+
query = {}
|
174
|
+
response = connection.delete("/nodes/#{node_id}", query, expects: [200, 406, 500], full_response: true)
|
175
|
+
if (response.status != 200)
|
176
|
+
raise "Error deleting node: HTTP-#{response.status} #{response.body}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.networks_on_host(connection, swarm)
|
181
|
+
networks = []
|
182
|
+
response = connection.get("/networks", {}, full_response: true, expects: [200])
|
183
|
+
network_hashes = JSON.parse(response.body)
|
184
|
+
network_hashes.each do |network_hash|
|
185
|
+
networks << Docker::Swarm::Network.new(swarm, network_hash)
|
186
|
+
end
|
187
|
+
return networks
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'docker-api'
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
class Docker::Swarm::Service
|
5
|
+
attr_reader :hash
|
6
|
+
|
7
|
+
def initialize(swarm, hash)
|
8
|
+
@swarm = swarm
|
9
|
+
@hash = hash
|
10
|
+
end
|
11
|
+
|
12
|
+
def name()
|
13
|
+
@hash['Spec']['Name']
|
14
|
+
end
|
15
|
+
|
16
|
+
def id()
|
17
|
+
return @hash['ID']
|
18
|
+
end
|
19
|
+
|
20
|
+
def reload()
|
21
|
+
s = @swarm.find_service(id())
|
22
|
+
@hash = s.hash
|
23
|
+
return self
|
24
|
+
end
|
25
|
+
|
26
|
+
def network_ids
|
27
|
+
network_ids = []
|
28
|
+
if (@hash['Endpoint']['VirtualIPs'])
|
29
|
+
@hash['Endpoint']['VirtualIPs'].each do |network_info|
|
30
|
+
network_ids << network_info['NetworkID']
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return network_ids
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove(opts = {})
|
37
|
+
query = {}
|
38
|
+
@swarm.connection.delete("/services/#{self.id}", query, :body => opts.to_json)
|
39
|
+
end
|
40
|
+
|
41
|
+
def update(options = {})
|
42
|
+
specs = @hash['Spec'].deep_merge(options)
|
43
|
+
query = {}
|
44
|
+
version = @hash['Version']['Index']
|
45
|
+
response = @swarm.connection.post("/services/#{self.id}/update?version=#{version}", query, :body => specs.to_json)
|
46
|
+
end
|
47
|
+
|
48
|
+
def restart
|
49
|
+
options = {}
|
50
|
+
options['TaskTemplate'] = {'ForceUpdate' => 1}
|
51
|
+
update(options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def scale(count)
|
55
|
+
@hash['Spec']['Mode']['Replicated']['Replicas'] = count
|
56
|
+
self.update(@hash['Spec'])
|
57
|
+
end
|
58
|
+
|
59
|
+
def replicas
|
60
|
+
@hash['Spec']['Mode']['Replicated']['Replicas']
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.DEFAULT_OPTIONS
|
64
|
+
default_service_create_options = {
|
65
|
+
"Name" => "<<Required>>",
|
66
|
+
"TaskTemplate" => {
|
67
|
+
"ContainerSpec" => {
|
68
|
+
"Image" => "<<Required>>",
|
69
|
+
"Mounts" => [],
|
70
|
+
"User" => "root"
|
71
|
+
},
|
72
|
+
"Env" => [],
|
73
|
+
"LogDriver" => {
|
74
|
+
"Name" => "json-file",
|
75
|
+
"Options" => {
|
76
|
+
"max-file" => "3",
|
77
|
+
"max-size" => "10M"
|
78
|
+
}
|
79
|
+
},
|
80
|
+
"Placement" => {},
|
81
|
+
"Resources" => {
|
82
|
+
"Limits" => {
|
83
|
+
"MemoryBytes" => 104857600
|
84
|
+
},
|
85
|
+
"Reservations" => {
|
86
|
+
# "NanoCPUs" => ?
|
87
|
+
# MemoryBytes =>
|
88
|
+
}
|
89
|
+
},
|
90
|
+
"RestartPolicy" => {
|
91
|
+
"Condition" => "on-failure",
|
92
|
+
"Delay" => 1,
|
93
|
+
"MaxAttempts" => 3
|
94
|
+
}
|
95
|
+
}, # End of TaskTemplate
|
96
|
+
"Mode" => {
|
97
|
+
"Replicated" => {
|
98
|
+
"Replicas" => 1
|
99
|
+
}
|
100
|
+
},
|
101
|
+
"UpdateConfig" => {
|
102
|
+
"Delay" => 2,
|
103
|
+
"Parallelism" => 2,
|
104
|
+
"FailureAction" => "pause"
|
105
|
+
},
|
106
|
+
"EndpointSpec" => {
|
107
|
+
"Ports" => [
|
108
|
+
{
|
109
|
+
# "Protocol" => "http",
|
110
|
+
# "PublishedPort" => 2881,
|
111
|
+
# "TargetPort" => 2881
|
112
|
+
}
|
113
|
+
]
|
114
|
+
},
|
115
|
+
"Labels" => {
|
116
|
+
"foo" => "bar"
|
117
|
+
}
|
118
|
+
}
|
119
|
+
return default_service_create_options
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,374 @@
|
|
1
|
+
require 'docker-api'
|
2
|
+
require 'resolv'
|
3
|
+
|
4
|
+
|
5
|
+
# This class represents a Docker Swarm Node.
|
6
|
+
class Docker::Swarm::Swarm
|
7
|
+
include Docker
|
8
|
+
attr_reader :node_ip, :manager_ip, :worker_join_token, :manager_join_token, :id, :hash, :node_hash
|
9
|
+
|
10
|
+
def store_manager(manager_connection, listen_address_and_port)
|
11
|
+
node = nodes.find {|n|
|
12
|
+
(n.hash['ManagerStatus']) && (n.hash['ManagerStatus']['Leader'] == true) && (n.hash['ManagerStatus']['Addr'] == listen_address_and_port)
|
13
|
+
}
|
14
|
+
raise "Node not found for: #{listen_address}" if (!node)
|
15
|
+
@node_hash[node.id] = {hash: node.hash, connection: manager_connection}
|
16
|
+
end
|
17
|
+
|
18
|
+
def update_data(hash)
|
19
|
+
@hash = hash
|
20
|
+
end
|
21
|
+
|
22
|
+
def socket_connection(node_connection)
|
23
|
+
node_connection.url.include?('unix:///')
|
24
|
+
end
|
25
|
+
|
26
|
+
def join(node_connection, node_ip = nil, manager_ip = nil, join_token = nil, listen_address = "0.0.0.0:2377")
|
27
|
+
node_ids_before = []
|
28
|
+
query = {}
|
29
|
+
|
30
|
+
unless socket_connection(node_connection)
|
31
|
+
node_ids_before = nodes().collect {|n| n.id}
|
32
|
+
node_ip = node_connection.url.split("//").last.split(":").first
|
33
|
+
manager_ip = self.connection.url.split("//").last.split(":").first
|
34
|
+
end
|
35
|
+
|
36
|
+
join_options = {
|
37
|
+
"ListenAddr" => "#{listen_address}",
|
38
|
+
"AdvertiseAddr" => "#{node_ip}:2377",
|
39
|
+
"RemoteAddrs" => ["#{manager_ip}:2377"],
|
40
|
+
"JoinToken" => join_token
|
41
|
+
}
|
42
|
+
|
43
|
+
new_node = nil
|
44
|
+
response = node_connection.post('/swarm/join', query, :body => join_options.to_json, expects: [200, 406, 500], full_response: true)
|
45
|
+
|
46
|
+
if (response.status == 200)
|
47
|
+
nodes.each do |node|
|
48
|
+
if (!node_ids_before.include? node.id)
|
49
|
+
new_node = node
|
50
|
+
@node_hash[node.id] = {hash: node.hash, connection: node_connection}
|
51
|
+
end
|
52
|
+
end unless socket_connection(node_connection)
|
53
|
+
return new_node
|
54
|
+
elsif (response.status == 406)
|
55
|
+
puts "Node is already part of a swarm - maybe this swarm, maybe another swarm."
|
56
|
+
return nil
|
57
|
+
else
|
58
|
+
raise "Error joining (#{node_connection}): HTTP-#{response.status} #{response.body}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def join_worker(node_connection, listen_address = "0.0.0.0:2377")
|
63
|
+
join(node_connection, @node_ip, @manager_ip, @worker_join_token)
|
64
|
+
end
|
65
|
+
|
66
|
+
def join_manager(node_connection, listen_address = "0.0.0.0:2377")
|
67
|
+
join(node_connection, @node_ip, @manager_ip, @manager_join_token, listen_address)
|
68
|
+
end
|
69
|
+
|
70
|
+
def connection
|
71
|
+
@node_hash.keys.each do |node_id|
|
72
|
+
node_info = @node_hash[node_id]
|
73
|
+
if (node_info[:hash]['ManagerStatus'])
|
74
|
+
return node_info[:connection]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
return @manager_connection
|
78
|
+
end
|
79
|
+
|
80
|
+
def remove
|
81
|
+
services().each do |service|
|
82
|
+
service.remove()
|
83
|
+
end
|
84
|
+
|
85
|
+
worker_nodes.each do |node|
|
86
|
+
leave(node, true)
|
87
|
+
end
|
88
|
+
manager_nodes.each do |node|
|
89
|
+
leave(node, true)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def tasks
|
94
|
+
items = []
|
95
|
+
query = {}
|
96
|
+
opts = {}
|
97
|
+
resp = self.connection.get('/tasks', query, :body => opts.to_json)
|
98
|
+
hashes = JSON.parse(resp)
|
99
|
+
items = []
|
100
|
+
hashes.each do |hash|
|
101
|
+
items << Swarm::Task.new(self, hash)
|
102
|
+
end
|
103
|
+
return items
|
104
|
+
end
|
105
|
+
|
106
|
+
def leave(node, force = false)
|
107
|
+
node_info = @node_hash[node.id]
|
108
|
+
if (node_info)
|
109
|
+
Docker::Swarm::Swarm.leave(force, node_info[:connection])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def remove_node(worker_node)
|
114
|
+
Swarm::Node.remove(worker_node.id, self.connection)
|
115
|
+
end
|
116
|
+
|
117
|
+
def manager_nodes
|
118
|
+
return nodes.select { |node| node.role == :manager} || []
|
119
|
+
end
|
120
|
+
|
121
|
+
def worker_nodes
|
122
|
+
return nodes.select { |node| node.role == :worker} || []
|
123
|
+
end
|
124
|
+
|
125
|
+
def networks
|
126
|
+
all_networks = []
|
127
|
+
response = connection.get("/networks", {}, full_response: true)
|
128
|
+
if (response.status == 200)
|
129
|
+
hashes = JSON.parse(response.body)
|
130
|
+
hashes.each do |hash|
|
131
|
+
all_networks << Docker::Swarm::Network.new(self, hash)
|
132
|
+
end
|
133
|
+
else
|
134
|
+
raise "Error finding netw"
|
135
|
+
end
|
136
|
+
return all_networks
|
137
|
+
end
|
138
|
+
|
139
|
+
def create_network(options)
|
140
|
+
response = connection.post('/networks/create', {}, body: options.to_json, expects: [200, 201, 500], full_response: true)
|
141
|
+
if (response.status <= 201)
|
142
|
+
hash = JSON.parse(response.body)
|
143
|
+
response = connection.get("/networks/#{hash['Id']}", {}, expects: [200, 201], full_response: true)
|
144
|
+
hash = Docker::Util.parse_json(response.body)
|
145
|
+
network = Docker::Swarm::Network.new(self, hash)
|
146
|
+
return network
|
147
|
+
else
|
148
|
+
raise "Error creating network: HTTP-#{response.status} - #{response.body}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_network_overlay(network_name)
|
153
|
+
subnet_16_parts = [10, 10, 0, 0]
|
154
|
+
max_vxlanid = 200
|
155
|
+
|
156
|
+
# Sometimes nodes have leftover networks not on other nodes, that have subnets that can't be duplicated in
|
157
|
+
# the new overlay network.
|
158
|
+
nodes.each do |node|
|
159
|
+
node.networks.each do |network|
|
160
|
+
if (network.driver == 'overlay')
|
161
|
+
if (network.hash['Options'])
|
162
|
+
vxlanid = network.hash['Options']["com.docker.network.driver.overlay.vxlanid_list"]
|
163
|
+
if (vxlanid) && (vxlanid.to_i > max_vxlanid)
|
164
|
+
max_vxlanid = vxlanid.to_i
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Make sure our new network doesn't duplicate subnet of other network.
|
170
|
+
if (network.hash['IPAM']) && (network.hash['IPAM']['Config'])
|
171
|
+
network.hash['IPAM']['Config'].each do |subnet_config|
|
172
|
+
if (subnet_config['Subnet'])
|
173
|
+
subnet = subnet_config['Subnet']
|
174
|
+
subnet = subnet.split(".")
|
175
|
+
if (subnet[0] == '10') && (subnet[1] == '255')
|
176
|
+
else
|
177
|
+
if (subnet[0].to_i == subnet_16_parts[0])
|
178
|
+
if (subnet[1].to_i >= subnet_16_parts[1])
|
179
|
+
subnet_16_parts[1] = subnet[1].to_i + 1
|
180
|
+
if (subnet_16_parts[1] >= 255)
|
181
|
+
raise "Ran out of subnets"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
# subnet_config['Gateway']
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
options = {
|
195
|
+
"Name" => network_name,
|
196
|
+
"CheckDuplicate" => true,
|
197
|
+
"Driver" => "overlay",
|
198
|
+
"EnableIPv6" => false,
|
199
|
+
"IPAM" => {
|
200
|
+
"Driver" => "default",
|
201
|
+
"Config" => [
|
202
|
+
{
|
203
|
+
"Subnet" => "#{subnet_16_parts.join(".")}/16",
|
204
|
+
"Gateway"=> "#{subnet_16_parts[0, 3].join('.')}.1"
|
205
|
+
}
|
206
|
+
],
|
207
|
+
"Options" => {
|
208
|
+
}
|
209
|
+
},
|
210
|
+
"Internal" => false,
|
211
|
+
"Options" => {
|
212
|
+
"com.docker.network.driver.overlay.vxlanid_list" => (max_vxlanid + 1).to_s
|
213
|
+
},
|
214
|
+
"Labels" => {
|
215
|
+
# "com.example.some-label": "some-value",
|
216
|
+
# "com.example.some-other-label": "some-other-value"
|
217
|
+
}
|
218
|
+
}
|
219
|
+
create_network(options)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Return all of the Nodes.
|
223
|
+
def nodes
|
224
|
+
opts = {}
|
225
|
+
query = {}
|
226
|
+
response = self.connection.get('/nodes', query, :body => opts.to_json, expects: [200, 406], full_response: true)
|
227
|
+
if (response.status == 200)
|
228
|
+
hashes = JSON.parse(response.body)
|
229
|
+
nodes = []
|
230
|
+
hashes.each do |node_hash|
|
231
|
+
node = Docker::Swarm::Node.new(self, node_hash)
|
232
|
+
nodes << node
|
233
|
+
end
|
234
|
+
return nodes || []
|
235
|
+
else
|
236
|
+
return []
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def create_service(opts = {})
|
241
|
+
query = {}
|
242
|
+
response = self.connection.post('/services/create', query, :body => opts.to_json, expects: [201, 404, 409, 500], full_response: true)
|
243
|
+
if (response.status <= 201)
|
244
|
+
info = JSON.parse(response.body)
|
245
|
+
service_id = info['ID']
|
246
|
+
return self.find_service(service_id)
|
247
|
+
else
|
248
|
+
raise "Error creating service: HTTP-#{response.status} #{response.body}"
|
249
|
+
end
|
250
|
+
return nil
|
251
|
+
end
|
252
|
+
|
253
|
+
def find_service(id)
|
254
|
+
query = {}
|
255
|
+
opts = {}
|
256
|
+
response = self.connection.get("/services/#{id}", query, :body => opts.to_json)
|
257
|
+
hash = JSON.parse(response)
|
258
|
+
return Docker::Swarm::Service.new(self, hash)
|
259
|
+
end
|
260
|
+
|
261
|
+
def find_service_by_name(name)
|
262
|
+
services.each do |service|
|
263
|
+
return service if (service.name == name)
|
264
|
+
end
|
265
|
+
return nil
|
266
|
+
end
|
267
|
+
|
268
|
+
def services
|
269
|
+
items = []
|
270
|
+
query = {}
|
271
|
+
opts = {}
|
272
|
+
response = self.connection.get("/services", query, :body => opts.to_json)
|
273
|
+
hashes = JSON.parse(response)
|
274
|
+
hashes.each do |hash|
|
275
|
+
items << Docker::Swarm::Service.new(self, hash)
|
276
|
+
end
|
277
|
+
return items
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
# Initialize Swarm
|
282
|
+
def self.init(opts, connection)
|
283
|
+
query = {}
|
284
|
+
resp = connection.post('/swarm/init', query, :body => opts.to_json, full_response: true, expects: [200, 404, 406, 500])
|
285
|
+
if (resp.status == 200)
|
286
|
+
swarm = Docker::Swarm::Swarm.swarm(opts, connection)
|
287
|
+
manager_node = swarm.nodes.find {|n|
|
288
|
+
(n.hash['ManagerStatus']) && (n.hash['ManagerStatus']['Leader'] == true)
|
289
|
+
}
|
290
|
+
byebug
|
291
|
+
listen_address = manager_node.hash['ManagerStatus']['Addr']
|
292
|
+
swarm.store_manager(connection, listen_address)
|
293
|
+
return swarm
|
294
|
+
else
|
295
|
+
raise "Bad response: #{resp.status} #{resp.body}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# docker swarm join-token -q worker
|
300
|
+
def self.swarm(connection, options = {})
|
301
|
+
query = {}
|
302
|
+
resp = connection.get('/swarm', query, :body => options.to_json, expects: [200, 404, 406], full_response: true)
|
303
|
+
if (resp.status == 406) || (resp.status == 404)
|
304
|
+
return nil
|
305
|
+
elsif (resp.status == 200)
|
306
|
+
hash = JSON.parse(resp.body)
|
307
|
+
swarm = self.find_swarm_for_id(hash['ID'])
|
308
|
+
if (swarm)
|
309
|
+
swarm.update_data(hash)
|
310
|
+
else
|
311
|
+
swarm = Docker::Swarm::Swarm.new(hash, connection, options)
|
312
|
+
end
|
313
|
+
else
|
314
|
+
raise "Bad response: #{resp.status} #{resp.body}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.leave(force, connection)
|
319
|
+
query = {}
|
320
|
+
query['force'] = force
|
321
|
+
response = connection.post('/swarm/leave', query, expects: [200, 406, 500], full_response: true)
|
322
|
+
if (response.status == 500)
|
323
|
+
raise "Error leaving: #{response.body} HTTP-#{response.status}"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def self.find(connection, options = {})
|
328
|
+
query = {}
|
329
|
+
response = connection.get('/swarm', query, expects: [200, 404, 406], full_response: true)
|
330
|
+
if (response.status == 200)
|
331
|
+
hash = JSON.parse(response.body)
|
332
|
+
swarm = self.find_swarm_for_id(hash['ID'])
|
333
|
+
if (swarm)
|
334
|
+
swarm.update_data(hash)
|
335
|
+
else
|
336
|
+
swarm = Docker::Swarm::Swarm.new(hash, connection, options)
|
337
|
+
end
|
338
|
+
manager_node = swarm.nodes.find {|n|
|
339
|
+
(n.hash['ManagerStatus']) && (n.hash['ManagerStatus']['Leader'] == true)
|
340
|
+
}
|
341
|
+
listen_address = manager_node.hash['ManagerStatus']['Addr']
|
342
|
+
swarm.store_manager(connection, listen_address)
|
343
|
+
return swarm
|
344
|
+
elsif (response.status > 200)
|
345
|
+
return nil
|
346
|
+
else
|
347
|
+
raise "Error finding swarm: HTTP-#{response.status} #{response.body}"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
private
|
353
|
+
@@swarms = {}
|
354
|
+
|
355
|
+
def self.find_swarm_for_id(swarm_id)
|
356
|
+
return @@swarms[swarm_id]
|
357
|
+
end
|
358
|
+
|
359
|
+
def initialize(hash, manager_connection = nil, options = {})
|
360
|
+
@hash = hash
|
361
|
+
@id = hash['ID']
|
362
|
+
@node_ip = hash['node_ip']
|
363
|
+
@manager_ip = hash['manager_ip']
|
364
|
+
@worker_join_token = hash['JoinTokens']['Worker']
|
365
|
+
@manager_join_token = hash['JoinTokens']['Manager']
|
366
|
+
@node_hash = {}
|
367
|
+
@manager_connection = manager_connection
|
368
|
+
@@swarms[@id] = self
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
|
373
|
+
|
374
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# This class represents a Docker Swarm Node.
|
2
|
+
class Docker::Swarm::Task
|
3
|
+
#include Docker::Base
|
4
|
+
attr_reader :hash
|
5
|
+
|
6
|
+
def initialize(swarm, hash)
|
7
|
+
@hash = hash
|
8
|
+
@swarm = swarm
|
9
|
+
end
|
10
|
+
|
11
|
+
def id
|
12
|
+
return @hash['ID']
|
13
|
+
end
|
14
|
+
|
15
|
+
def image
|
16
|
+
return @hash['Spec']['ContainerSpec']['Image']
|
17
|
+
end
|
18
|
+
|
19
|
+
def service_id
|
20
|
+
@hash['ServiceID']
|
21
|
+
end
|
22
|
+
|
23
|
+
def service
|
24
|
+
return @swarm.services.find { |service|
|
25
|
+
self.service_id == service.id
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def node_id
|
30
|
+
@hash['NodeID']
|
31
|
+
end
|
32
|
+
|
33
|
+
def node
|
34
|
+
return @swarm.nodes.find {|n| n.id == self.node_id}
|
35
|
+
end
|
36
|
+
|
37
|
+
def created_at
|
38
|
+
return DateTime.parse(@hash.first['CreatedAt'])
|
39
|
+
end
|
40
|
+
|
41
|
+
def status
|
42
|
+
@hash['Status']['State'].to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
def status_timestamp
|
46
|
+
return DateTime.parse(@hash['Status']['Timestamp'])
|
47
|
+
end
|
48
|
+
|
49
|
+
def status_message
|
50
|
+
@hash['Status']['Message']
|
51
|
+
end
|
52
|
+
|
53
|
+
def networks
|
54
|
+
all_networks = @swarm.networks
|
55
|
+
nets = []
|
56
|
+
self.hash['NetworksAttachments'].each do |net_hash|
|
57
|
+
hash = net_hash['Network']
|
58
|
+
network_id = hash['ID']
|
59
|
+
nets << all_networks.find {|net| net.id == network_id}
|
60
|
+
end
|
61
|
+
return nets
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: docker-swarm-sdk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Moore
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: docker-api
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.33.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.33.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: retry_block
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.2.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.2.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '6.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '6.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '12.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '12.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-its
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.2'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.2'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.10.4
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.10.4
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: single_cov
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.5.8
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.5.8
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: parallel
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '1.10'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '1.10'
|
167
|
+
description: API for creating container clusters and services using Docker Swarm. Includes
|
168
|
+
service, node, task management
|
169
|
+
email: m.moore.denver@gmail.com
|
170
|
+
executables: []
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files: []
|
173
|
+
files:
|
174
|
+
- LICENSE
|
175
|
+
- README.md
|
176
|
+
- lib/docker-swarm-sdk.rb
|
177
|
+
- lib/docker-swarm.rb
|
178
|
+
- lib/docker/swarm/connection.rb
|
179
|
+
- lib/docker/swarm/network.rb
|
180
|
+
- lib/docker/swarm/node.rb
|
181
|
+
- lib/docker/swarm/service.rb
|
182
|
+
- lib/docker/swarm/swarm.rb
|
183
|
+
- lib/docker/swarm/task.rb
|
184
|
+
- lib/docker/swarm/version.rb
|
185
|
+
homepage: https://github.com/mikejmoore/docker-swarm-api
|
186
|
+
licenses:
|
187
|
+
- MIT
|
188
|
+
metadata: {}
|
189
|
+
post_install_message:
|
190
|
+
rdoc_options: []
|
191
|
+
require_paths:
|
192
|
+
- lib
|
193
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
194
|
+
requirements:
|
195
|
+
- - ">="
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
requirements:
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: '0'
|
203
|
+
requirements: []
|
204
|
+
rubyforge_project:
|
205
|
+
rubygems_version: 2.6.12
|
206
|
+
signing_key:
|
207
|
+
specification_version: 4
|
208
|
+
summary: Ruby API for Docker Swarm
|
209
|
+
test_files: []
|