centurion 1.6.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- add_to_bindings(
47
- options[:host_ip],
48
- options[:container_port],
49
- port,
50
- options[:type] || 'tcp'
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 = fetch(:binds, [])
65
- container_volume = options[:container_volume]
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
- :old_containers_for_port, :remove_container
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]{7})?\Z/
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(
@@ -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 { |ext|
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