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 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'
@@ -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
@@ -0,0 +1,9 @@
1
+ module Docker
2
+ module Swarm
3
+ # The version of this gem.
4
+ VERSION = '1.2.5'
5
+
6
+ # The version of the compatible Docker remote API.
7
+ API_VERSION = '1.24'
8
+ end
9
+ 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: []