centurion 1.6.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|