centurion 1.6.0 → 1.8.0
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/CONTRIBUTORS.md +2 -0
- data/README.md +100 -15
- data/lib/centurion/deploy.rb +36 -134
- data/lib/centurion/deploy_dsl.rb +62 -21
- data/lib/centurion/docker_server.rb +12 -2
- data/lib/centurion/docker_via_api.rb +18 -10
- data/lib/centurion/dogestry.rb +16 -2
- data/lib/centurion/service.rb +218 -0
- data/lib/centurion/version.rb +1 -1
- data/lib/core_ext/numeric_bytes.rb +59 -57
- data/lib/tasks/centurion.rake +3 -0
- data/lib/tasks/deploy.rake +29 -42
- data/spec/deploy_dsl_spec.rb +78 -17
- data/spec/deploy_spec.rb +45 -343
- data/spec/docker_server_spec.rb +16 -1
- data/spec/docker_via_api_spec.rb +32 -55
- data/spec/service_spec.rb +288 -0
- metadata +27 -42
data/lib/centurion/deploy_dsl.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require_relative 'docker_server_group'
|
2
|
+
require_relative 'docker_server'
|
3
|
+
require_relative 'service'
|
2
4
|
require 'uri'
|
3
5
|
|
4
6
|
module Centurion::DeployDSL
|
@@ -14,6 +16,22 @@ module Centurion::DeployDSL
|
|
14
16
|
set(:env_vars, current)
|
15
17
|
end
|
16
18
|
|
19
|
+
def add_capability(new_cap_adds)
|
20
|
+
if !valid_capability?(new_cap_adds)
|
21
|
+
abort("Invalid capability addition #{new_cap_adds} specified.")
|
22
|
+
end
|
23
|
+
current = fetch(:cap_adds, [])
|
24
|
+
set(:cap_adds, current << new_cap_adds)
|
25
|
+
end
|
26
|
+
|
27
|
+
def drop_capability(new_cap_drops)
|
28
|
+
if !valid_capability?(new_cap_drops)
|
29
|
+
abort("Invalid capability drop #{new_cap_drops} specified.")
|
30
|
+
end
|
31
|
+
current = fetch(:cap_drops, [])
|
32
|
+
set(:cap_drops, current << new_cap_drops)
|
33
|
+
end
|
34
|
+
|
17
35
|
def host(hostname)
|
18
36
|
current = fetch(:hosts, [])
|
19
37
|
current << hostname
|
@@ -43,12 +61,17 @@ module Centurion::DeployDSL
|
|
43
61
|
validate_options_keys(options, [ :host_ip, :container_port, :type ])
|
44
62
|
require_options_keys(options, [ :container_port ])
|
45
63
|
|
46
|
-
|
47
|
-
options[:host_ip]
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
64
|
+
set(:port_bindings, fetch(:port_bindings, []).tap do |bindings|
|
65
|
+
bindings << Centurion::Service::PortBinding.new(port, options[:container_port], options[:type] || 'tcp', options[:host_ip])
|
66
|
+
end)
|
67
|
+
end
|
68
|
+
|
69
|
+
def network_mode(mode)
|
70
|
+
if %w(bridge host).include?(mode) or mode =~ /container.*/
|
71
|
+
set(:network_mode, mode)
|
72
|
+
else
|
73
|
+
abort("invalid value for network_mode: #{mode}, value must be one of 'bridge', 'host', or 'container:<name|id>'")
|
74
|
+
end
|
52
75
|
end
|
53
76
|
|
54
77
|
def public_port_for(port_bindings)
|
@@ -61,11 +84,9 @@ module Centurion::DeployDSL
|
|
61
84
|
validate_options_keys(options, [ :container_volume ])
|
62
85
|
require_options_keys(options, [ :container_volume ])
|
63
86
|
|
64
|
-
binds
|
65
|
-
|
66
|
-
|
67
|
-
binds << "#{volume}:#{container_volume}"
|
68
|
-
set(:binds, binds)
|
87
|
+
set(:binds, fetch(:binds, []).tap do |volumes|
|
88
|
+
volumes << Centurion::Service::Volume.new(volume, options[:container_volume])
|
89
|
+
end)
|
69
90
|
end
|
70
91
|
|
71
92
|
def get_current_tags_for(image)
|
@@ -85,6 +106,27 @@ module Centurion::DeployDSL
|
|
85
106
|
set(:health_check, method)
|
86
107
|
end
|
87
108
|
|
109
|
+
def extra_host(ip, name)
|
110
|
+
current = fetch(:extra_hosts, [])
|
111
|
+
current.push("#{name}:#{ip}")
|
112
|
+
set(:extra_hosts, current)
|
113
|
+
end
|
114
|
+
|
115
|
+
def defined_service
|
116
|
+
Centurion::Service.from_env
|
117
|
+
end
|
118
|
+
|
119
|
+
def defined_health_check
|
120
|
+
Centurion::HealthCheck.new(fetch(:health_check, method(:http_status_ok?)),
|
121
|
+
fetch(:status_endpoint, '/'),
|
122
|
+
fetch(:rolling_deploy_wait_time, 5),
|
123
|
+
fetch(:rolling_deploy_retries, 24))
|
124
|
+
end
|
125
|
+
|
126
|
+
def defined_restart_policy
|
127
|
+
Centurion::Service::RestartPolicy.new(fetch(:restart_policy_name, 'on-failure'), fetch(:restart_policy_max_retry_count, 10))
|
128
|
+
end
|
129
|
+
|
88
130
|
private
|
89
131
|
|
90
132
|
def build_server_group
|
@@ -92,16 +134,6 @@ module Centurion::DeployDSL
|
|
92
134
|
Centurion::DockerServerGroup.new(hosts, docker_path, build_tls_params)
|
93
135
|
end
|
94
136
|
|
95
|
-
def add_to_bindings(host_ip, container_port, port, type='tcp')
|
96
|
-
set(:port_bindings, fetch(:port_bindings, {}).tap do |bindings|
|
97
|
-
binding = { 'HostPort' => port.to_s }.tap do |b|
|
98
|
-
b['HostIp'] = host_ip if host_ip
|
99
|
-
end
|
100
|
-
bindings["#{container_port.to_s}/#{type}"] = [ binding ]
|
101
|
-
bindings
|
102
|
-
end)
|
103
|
-
end
|
104
|
-
|
105
137
|
def validate_options_keys(options, valid_keys)
|
106
138
|
unless options.keys.all? { |k| valid_keys.include?(k) }
|
107
139
|
raise ArgumentError.new('Options passed with invalid key!')
|
@@ -116,6 +148,15 @@ module Centurion::DeployDSL
|
|
116
148
|
end
|
117
149
|
end
|
118
150
|
|
151
|
+
def valid_capability?(capability)
|
152
|
+
%w(ALL SETPCAP SYS_MODULE SYS_RAWIO SYS_PACCT SYS_ADMIN SYS_NICE
|
153
|
+
SYS_RESOURCE SYS_TIME SYS_TTY_CONFIG MKNOD AUDIT_WRITE AUDIT_CONTROL
|
154
|
+
MAC_OVERRIDE MAC_ADMIN NET_ADMIN SYSLOG CHOWN NET_RAW DAC_OVERRIDE FOWNER
|
155
|
+
DAC_READ_SEARCH FSETID KILL SETGID SETUID LINUX_IMMUTABLE
|
156
|
+
NET_BIND_SERVICE NET_BROADCAST IPC_LOCK IPC_OWNER SYS_CHROOT SYS_PTRACE
|
157
|
+
SYS_BOOT LEASE SETFCAP WAKE_ALARM BLOCK_SUSPEND).include?(capability)
|
158
|
+
end
|
159
|
+
|
119
160
|
def tls_paths_available?
|
120
161
|
Centurion::DockerViaCli.tls_keys.all? { |key| fetch(key).present? }
|
121
162
|
end
|
@@ -15,7 +15,7 @@ class Centurion::DockerServer
|
|
15
15
|
|
16
16
|
def_delegators :docker_via_api, :create_container, :inspect_container,
|
17
17
|
:inspect_image, :ps, :start_container, :stop_container,
|
18
|
-
:
|
18
|
+
:remove_container, :restart_container
|
19
19
|
def_delegators :docker_via_cli, :pull, :tail, :attach
|
20
20
|
|
21
21
|
def initialize(host, docker_path, tls_params = {})
|
@@ -45,11 +45,21 @@ class Centurion::DockerServer
|
|
45
45
|
ps.select do |container|
|
46
46
|
next unless container && container['Names']
|
47
47
|
container['Names'].find do |name|
|
48
|
-
name =~ /\A\/#{wanted_name}(-[a-f0-9]{
|
48
|
+
name =~ /\A\/#{wanted_name}(-[a-f0-9]{14})?\Z/
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
+
def find_container_by_id(container_id)
|
54
|
+
ps.find { |container| container && container['Id'] == container_id }
|
55
|
+
end
|
56
|
+
|
57
|
+
def old_containers_for_name(wanted_name)
|
58
|
+
find_containers_by_name(wanted_name).select do |container|
|
59
|
+
container["Status"] =~ /^(Exit |Exited)/
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
53
63
|
private
|
54
64
|
|
55
65
|
def docker_via_api
|
@@ -34,16 +34,6 @@ class Centurion::DockerViaApi
|
|
34
34
|
JSON.load(response.body)
|
35
35
|
end
|
36
36
|
|
37
|
-
def old_containers_for_port(host_port)
|
38
|
-
old_containers = ps(all: true).select do |container|
|
39
|
-
container["Status"] =~ /^(Exit |Exited)/
|
40
|
-
end.select do |container|
|
41
|
-
inspected = inspect_container container["Id"]
|
42
|
-
container_listening_on_port?(inspected, host_port)
|
43
|
-
end
|
44
|
-
old_containers
|
45
|
-
end
|
46
|
-
|
47
37
|
def remove_container(container_id)
|
48
38
|
path = "/v1.7/containers/#{container_id}"
|
49
39
|
response = Excon.delete(
|
@@ -97,6 +87,24 @@ class Centurion::DockerViaApi
|
|
97
87
|
end
|
98
88
|
end
|
99
89
|
|
90
|
+
def restart_container(container_id, timeout = 30)
|
91
|
+
path = "/v1.10/containers/#{container_id}/restart?t=#{timeout}"
|
92
|
+
response = Excon.post(
|
93
|
+
@base_uri + path,
|
94
|
+
tls_excon_arguments
|
95
|
+
)
|
96
|
+
case response.status
|
97
|
+
when 204
|
98
|
+
true
|
99
|
+
when 404
|
100
|
+
fail "Failed to start missing container! \"#{response.body}\""
|
101
|
+
when 500
|
102
|
+
fail "Failed to start existing container! \"#{response.body}\""
|
103
|
+
else
|
104
|
+
raise response.inspect
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
100
108
|
def inspect_container(container_id)
|
101
109
|
path = "/v1.7/containers/#{container_id}/json"
|
102
110
|
response = Excon.get(
|
data/lib/centurion/dogestry.rb
CHANGED
@@ -17,10 +17,10 @@ class Centurion::Dogestry
|
|
17
17
|
def which(cmd)
|
18
18
|
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
19
19
|
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
20
|
-
exts.each
|
20
|
+
exts.each do |ext|
|
21
21
|
exe = File.join(path, "#{cmd}#{ext}")
|
22
22
|
return exe if File.executable?(exe) && !File.directory?(exe)
|
23
|
-
|
23
|
+
end
|
24
24
|
end
|
25
25
|
return nil
|
26
26
|
end
|
@@ -57,6 +57,20 @@ class Centurion::Dogestry
|
|
57
57
|
ENV['AWS_ACCESS_KEY'] = aws_access_key_id
|
58
58
|
ENV['AWS_SECRET_KEY'] = aws_secret_key
|
59
59
|
|
60
|
+
# If we want TLS, then try to pass a sane directory to Dogestry, which doesn't
|
61
|
+
# speak individual filenames.
|
62
|
+
if @options[:tlsverify]
|
63
|
+
ENV['DOCKER_CERT_PATH'] =
|
64
|
+
if @options[:tlscacert] || @options[:tlscert]
|
65
|
+
File.dirname(
|
66
|
+
@options[:tlscacert] ||
|
67
|
+
@options[:tlscert]
|
68
|
+
)
|
69
|
+
else
|
70
|
+
@options[:original_docker_cert_path]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
60
74
|
info "Dogestry ENV: #{ENV.inspect}"
|
61
75
|
end
|
62
76
|
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'capistrano_dsl'
|
3
|
+
|
4
|
+
module Centurion
|
5
|
+
class Service
|
6
|
+
extend ::Capistrano::DSL
|
7
|
+
|
8
|
+
attr_accessor :command, :dns, :extra_hosts, :image, :name, :volumes, :port_bindings, :network_mode, :cap_adds, :cap_drops
|
9
|
+
attr_reader :memory, :cpu_shares, :env_vars
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@env_vars = {}
|
14
|
+
@volumes = []
|
15
|
+
@port_bindings = []
|
16
|
+
@cap_adds = []
|
17
|
+
@cap_drops = []
|
18
|
+
@network_mode = 'bridge'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_env
|
22
|
+
Service.new(fetch(:name)).tap do |s|
|
23
|
+
s.image = if fetch(:tag, nil)
|
24
|
+
"#{fetch(:image, nil)}:#{fetch(:tag)}"
|
25
|
+
else
|
26
|
+
fetch(:image, nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
s.cap_adds = fetch(:cap_adds, [])
|
30
|
+
s.cap_drops = fetch(:cap_drops, [])
|
31
|
+
s.dns = fetch(:dns, nil)
|
32
|
+
s.extra_hosts = fetch(:extra_hosts, nil)
|
33
|
+
s.volumes = fetch(:binds, [])
|
34
|
+
s.port_bindings = fetch(:port_bindings, [])
|
35
|
+
s.network_mode = fetch(:network_mode, 'bridge')
|
36
|
+
s.command = fetch(:command, nil)
|
37
|
+
|
38
|
+
s.add_env_vars(fetch(:env_vars, {}))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_env_vars(new_vars)
|
43
|
+
@env_vars.merge!(new_vars)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_port_bindings(host_port, container_port, type = 'tcp', host_ip = nil)
|
47
|
+
@port_bindings << PortBinding.new(host_port, container_port, type, host_ip)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_volume(host_volume, container_volume)
|
51
|
+
@volumes << Volume.new(host_volume, container_volume)
|
52
|
+
end
|
53
|
+
|
54
|
+
def cap_adds=(capabilites)
|
55
|
+
unless capabilites.is_a? Array
|
56
|
+
raise ArgumentError, "invalid value for capability additions: #{capabilites}, value must be an array"
|
57
|
+
end
|
58
|
+
@cap_adds = capabilites
|
59
|
+
end
|
60
|
+
|
61
|
+
def cap_drops=(capabilites)
|
62
|
+
unless capabilites.is_a? Array
|
63
|
+
raise ArgumentError, "invalid value for capability drops: #{capabilites}, value must be an array"
|
64
|
+
end
|
65
|
+
@cap_drops = capabilites
|
66
|
+
end
|
67
|
+
|
68
|
+
def network_mode=(mode)
|
69
|
+
@network_mode = mode
|
70
|
+
end
|
71
|
+
|
72
|
+
def memory=(bytes)
|
73
|
+
if !bytes || !is_a_uint64?(bytes)
|
74
|
+
raise ArgumentError, "invalid value for cgroup memory constraint: #{bytes}, value must be a between 0 and 18446744073709551615"
|
75
|
+
end
|
76
|
+
@memory = bytes
|
77
|
+
end
|
78
|
+
|
79
|
+
def cpu_shares=(shares)
|
80
|
+
if !shares || !is_a_uint64?(shares)
|
81
|
+
raise ArgumentError, "invalid value for cgroup CPU constraint: #{shares}, value must be a between 0 and 18446744073709551615"
|
82
|
+
end
|
83
|
+
@cpu_shares = shares
|
84
|
+
end
|
85
|
+
|
86
|
+
def image=(image)
|
87
|
+
@image = image
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_config(server_hostname, &block)
|
91
|
+
container_config = {}.tap do |c|
|
92
|
+
c['Image'] = image
|
93
|
+
c['Hostname'] = block.call(server_hostname) if block_given?
|
94
|
+
c['Cmd'] = command if command
|
95
|
+
c['Memory'] = memory if memory
|
96
|
+
c['CpuShares'] = cpu_shares if cpu_shares
|
97
|
+
end
|
98
|
+
|
99
|
+
unless port_bindings.empty?
|
100
|
+
container_config['ExposedPorts'] = port_bindings.reduce({}) do |config, binding|
|
101
|
+
config["#{binding.container_port}/#{binding.type}"] = {}
|
102
|
+
config
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
unless env_vars.empty?
|
107
|
+
container_config['Env'] = env_vars.map do |k,v|
|
108
|
+
"#{k}=#{interpolate_var(v, server_hostname)}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
unless volumes.empty?
|
113
|
+
container_config['Volumes'] = volumes.inject({}) do |memo, v|
|
114
|
+
memo[v.container_volume] = {}
|
115
|
+
memo
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
container_config
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_host_config(restart_policy = nil)
|
123
|
+
host_config = {}
|
124
|
+
|
125
|
+
# Set capability additions and drops
|
126
|
+
host_config['CapAdd'] = cap_adds if cap_adds
|
127
|
+
host_config['CapDrop'] = cap_drops if cap_drops
|
128
|
+
|
129
|
+
# Map some host volumes if needed
|
130
|
+
host_config['Binds'] = volume_binds_config if volume_binds_config
|
131
|
+
|
132
|
+
# Bind the ports
|
133
|
+
host_config['PortBindings'] = port_bindings_config
|
134
|
+
|
135
|
+
# Set the network mode
|
136
|
+
host_config['NetworkMode'] = network_mode
|
137
|
+
|
138
|
+
# DNS if specified
|
139
|
+
host_config['Dns'] = dns if dns
|
140
|
+
|
141
|
+
# Add ExtraHosts if needed
|
142
|
+
host_config['ExtraHosts'] = extra_hosts if extra_hosts
|
143
|
+
|
144
|
+
# Restart Policy
|
145
|
+
if restart_policy
|
146
|
+
host_config['RestartPolicy'] = {}
|
147
|
+
|
148
|
+
restart_policy_name = restart_policy.name
|
149
|
+
restart_policy_name = 'on-failure' unless ["always", "on-failure", "no"].include?(restart_policy_name)
|
150
|
+
|
151
|
+
host_config['RestartPolicy']['Name'] = restart_policy_name
|
152
|
+
host_config['RestartPolicy']['MaximumRetryCount'] = restart_policy.max_retry_count || 10 if restart_policy_name == 'on-failure'
|
153
|
+
end
|
154
|
+
|
155
|
+
host_config
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_console_config(server_name, &block)
|
159
|
+
build_config(server_name, &block).merge({
|
160
|
+
'Cmd' => ['/bin/bash'],
|
161
|
+
'AttachStdin' => true,
|
162
|
+
'Tty' => true,
|
163
|
+
'OpenStdin' => true,
|
164
|
+
})
|
165
|
+
end
|
166
|
+
|
167
|
+
def volume_binds_config
|
168
|
+
@volumes.map { |volume| "#{volume.host_volume}:#{volume.container_volume}" }
|
169
|
+
end
|
170
|
+
|
171
|
+
def port_bindings_config
|
172
|
+
@port_bindings.inject({}) do |memo, binding|
|
173
|
+
config = {}
|
174
|
+
config['HostPort'] = binding.host_port.to_s
|
175
|
+
config['HostIp'] = binding.host_ip if binding.host_ip
|
176
|
+
memo["#{binding.container_port}/#{binding.type}"] = [config]
|
177
|
+
memo
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def public_ports
|
182
|
+
@port_bindings.map(&:host_port)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def is_a_uint64?(value)
|
188
|
+
result = false
|
189
|
+
if !value.is_a? Integer
|
190
|
+
return result
|
191
|
+
end
|
192
|
+
if value < 0 || value > 0xFFFFFFFFFFFFFFFF
|
193
|
+
return result
|
194
|
+
end
|
195
|
+
return true
|
196
|
+
end
|
197
|
+
|
198
|
+
def interpolate_var(val, hostname)
|
199
|
+
val.to_s.gsub('%DOCKER_HOSTNAME%', hostname)
|
200
|
+
.gsub('%DOCKER_HOST_IP%', host_ip(hostname))
|
201
|
+
end
|
202
|
+
|
203
|
+
def host_ip(hostname)
|
204
|
+
@host_ip ||= {}
|
205
|
+
return @host_ip[hostname] if @host_ip.has_key?(hostname)
|
206
|
+
@host_ip[hostname] = Socket.getaddrinfo(hostname, nil).first[2]
|
207
|
+
end
|
208
|
+
|
209
|
+
class RestartPolicy < Struct.new(:name, :max_retry_count)
|
210
|
+
end
|
211
|
+
|
212
|
+
class Volume < Struct.new(:host_volume, :container_volume)
|
213
|
+
end
|
214
|
+
|
215
|
+
class PortBinding < Struct.new(:host_port, :container_port, :type, :host_ip)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|