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.
@@ -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