docker-swarm-sdk 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
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: []