rory-deploy 1.8.4.1
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/.gitignore +10 -0
- data/CONTRIBUTORS.md +77 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.md +574 -0
- data/Rakefile +15 -0
- data/bin/rory +80 -0
- data/bin/rory-gen-config +79 -0
- data/lib/capistrano_dsl.rb +91 -0
- data/lib/centurion.rb +9 -0
- data/lib/centurion/deploy.rb +139 -0
- data/lib/centurion/deploy_dsl.rb +180 -0
- data/lib/centurion/docker_registry.rb +89 -0
- data/lib/centurion/docker_server.rb +79 -0
- data/lib/centurion/docker_server_group.rb +33 -0
- data/lib/centurion/docker_via_api.rb +166 -0
- data/lib/centurion/docker_via_cli.rb +81 -0
- data/lib/centurion/dogestry.rb +92 -0
- data/lib/centurion/logging.rb +28 -0
- data/lib/centurion/service.rb +218 -0
- data/lib/centurion/shell.rb +46 -0
- data/lib/centurion/version.rb +3 -0
- data/lib/core_ext/numeric_bytes.rb +94 -0
- data/lib/tasks/centurion.rake +15 -0
- data/lib/tasks/deploy.rake +250 -0
- data/lib/tasks/info.rake +24 -0
- data/lib/tasks/list.rake +56 -0
- data/rory-deploy.gemspec +33 -0
- data/spec/capistrano_dsl_spec.rb +67 -0
- data/spec/deploy_dsl_spec.rb +184 -0
- data/spec/deploy_spec.rb +212 -0
- data/spec/docker_registry_spec.rb +105 -0
- data/spec/docker_server_group_spec.rb +31 -0
- data/spec/docker_server_spec.rb +92 -0
- data/spec/docker_via_api_spec.rb +246 -0
- data/spec/docker_via_cli_spec.rb +91 -0
- data/spec/dogestry_spec.rb +73 -0
- data/spec/logging_spec.rb +41 -0
- data/spec/service_spec.rb +288 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/matchers/capistrano_dsl_matchers.rb +13 -0
- data/spec/support/matchers/exit_code_matches.rb +38 -0
- metadata +214 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'excon'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Centurion; end
|
6
|
+
|
7
|
+
class Centurion::DockerRegistry
|
8
|
+
OFFICIAL_URL = 'https://registry.hub.docker.com'
|
9
|
+
|
10
|
+
def initialize(base_uri, registry_user=nil, registry_password=nil)
|
11
|
+
@base_uri = base_uri
|
12
|
+
@user = registry_user
|
13
|
+
@password = registry_password
|
14
|
+
end
|
15
|
+
|
16
|
+
def digest_for_tag(repository, tag)
|
17
|
+
path = "/v1/repositories/#{repository}/tags/#{tag}"
|
18
|
+
uri = uri_for_repository_path(repository, path)
|
19
|
+
$stderr.puts "GET: #{uri}"
|
20
|
+
options = { headers: { "Content-Type" => "application/json" } }
|
21
|
+
if @user
|
22
|
+
options[:user] = @user
|
23
|
+
options[:password] = @password
|
24
|
+
end
|
25
|
+
response = Excon.get(
|
26
|
+
uri,
|
27
|
+
options
|
28
|
+
)
|
29
|
+
raise response.inspect unless response.status == 200
|
30
|
+
|
31
|
+
# This hack is stupid, and I hate it. But it works around the fact that
|
32
|
+
# the Docker Registry will return a base JSON String, which the Ruby parser
|
33
|
+
# refuses (possibly correctly) to handle
|
34
|
+
JSON.load('[' + response.body + ']').first
|
35
|
+
end
|
36
|
+
|
37
|
+
def repository_tags(repository)
|
38
|
+
path = "/v1/repositories/#{repository}/tags"
|
39
|
+
uri = uri_for_repository_path(repository, path)
|
40
|
+
|
41
|
+
$stderr.puts "GET: #{uri.inspect}"
|
42
|
+
|
43
|
+
# Need to workaround a bug in Docker Hub to now pass port in Host header
|
44
|
+
options = { omit_default_port: true }
|
45
|
+
|
46
|
+
if @user
|
47
|
+
options[:user] = @user
|
48
|
+
options[:password] = @password
|
49
|
+
end
|
50
|
+
|
51
|
+
response = Excon.get(uri, options)
|
52
|
+
raise response.inspect unless response.status == 200
|
53
|
+
|
54
|
+
tags = JSON.load(response.body)
|
55
|
+
|
56
|
+
# The Docker Registry API[1] specifies a result in the format
|
57
|
+
# { "[tag]" : "[image_id]" }. However, the official Docker registry returns a
|
58
|
+
# result like [{ "layer": "[image_id]", "name": "[tag]" }].
|
59
|
+
#
|
60
|
+
# So, we need to normalize the response to what the Docker Registry API
|
61
|
+
# specifies should be returned.
|
62
|
+
#
|
63
|
+
# [1]: https://docs.docker.com/v1.1/reference/api/registry_api/
|
64
|
+
|
65
|
+
if is_official_registry?(repository)
|
66
|
+
tags.each_with_object({}) do |tag, hash|
|
67
|
+
hash[tag['name']] = tag['layer']
|
68
|
+
end
|
69
|
+
else
|
70
|
+
tags
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def is_official_registry?(repository)
|
77
|
+
return @base_uri == OFFICIAL_URL
|
78
|
+
end
|
79
|
+
|
80
|
+
def uri_for_repository_path(repository, path)
|
81
|
+
if repository.match(/\A([a-z0-9]+[a-z0-9\-\.]+(?::[1-9][0-9]*)?)\/(.*)\z/)
|
82
|
+
host = $1
|
83
|
+
short_image_name = $2
|
84
|
+
"https://#{host}#{path.gsub(repository, short_image_name)}"
|
85
|
+
else
|
86
|
+
@base_uri + path
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'pty'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require_relative 'logging'
|
5
|
+
require_relative 'docker_via_api'
|
6
|
+
require_relative 'docker_via_cli'
|
7
|
+
|
8
|
+
module Centurion; end
|
9
|
+
|
10
|
+
class Centurion::DockerServer
|
11
|
+
include Centurion::Logging
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
attr_reader :hostname, :port
|
15
|
+
|
16
|
+
def_delegators :docker_via_api, :create_container, :inspect_container,
|
17
|
+
:inspect_image, :ps, :start_container, :stop_container,
|
18
|
+
:remove_container, :restart_container
|
19
|
+
def_delegators :docker_via_cli, :pull, :tail, :attach, :exec, :exec_it
|
20
|
+
|
21
|
+
def initialize(host, docker_path, tls_params = {})
|
22
|
+
@docker_path = docker_path
|
23
|
+
@hostname, @port = host.split(':')
|
24
|
+
@port ||= '2375'
|
25
|
+
@tls_params = tls_params
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_tags_for(image)
|
29
|
+
running_containers = ps.select { |c| c['Image'] =~ /#{image}/ }
|
30
|
+
return [] if running_containers.empty?
|
31
|
+
|
32
|
+
parse_image_tags_for(running_containers)
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_containers_by_public_port(public_port, type='tcp')
|
36
|
+
ps.select do |container|
|
37
|
+
next unless container && container['Ports']
|
38
|
+
container['Ports'].find do |port|
|
39
|
+
port['PublicPort'] == public_port.to_i && port['Type'] == type
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_containers_by_name(wanted_name)
|
45
|
+
ps.select do |container|
|
46
|
+
next unless container && container['Names']
|
47
|
+
container['Names'].find do |name|
|
48
|
+
name =~ /\A\/#{wanted_name}(-[a-f0-9]{14})?\Z/
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
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
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def docker_via_api
|
66
|
+
@docker_via_api ||= Centurion::DockerViaApi.new(@hostname, @port,
|
67
|
+
@tls_params, nil)
|
68
|
+
end
|
69
|
+
|
70
|
+
def docker_via_cli
|
71
|
+
@docker_via_cli ||= Centurion::DockerViaCli.new(@hostname, @port,
|
72
|
+
@docker_path, @tls_params)
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_image_tags_for(running_containers)
|
76
|
+
running_container_names = running_containers.map { |c| c['Image'] }
|
77
|
+
running_container_names.map { |name| name.split(/:/).last } # (image, tag)
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'docker_server'
|
2
|
+
require_relative 'logging'
|
3
|
+
|
4
|
+
module Centurion; end
|
5
|
+
|
6
|
+
class Centurion::DockerServerGroup
|
7
|
+
include Enumerable
|
8
|
+
include Centurion::Logging
|
9
|
+
|
10
|
+
attr_reader :hosts
|
11
|
+
|
12
|
+
def initialize(hosts, docker_path, tls_params = {})
|
13
|
+
raise ArgumentError.new('Bad Host list!') if hosts.nil? || hosts.empty?
|
14
|
+
@hosts = hosts.map do |hostname|
|
15
|
+
Centurion::DockerServer.new(hostname, docker_path, tls_params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(&block)
|
20
|
+
@hosts.each do |host|
|
21
|
+
info "----- Connecting to Docker on #{host.hostname} -----"
|
22
|
+
block.call(host)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_in_parallel(&block)
|
27
|
+
threads = @hosts.map do |host|
|
28
|
+
Thread.new { block.call(host) }
|
29
|
+
end
|
30
|
+
|
31
|
+
threads.each { |t| t.join }
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'excon'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Centurion; end
|
7
|
+
|
8
|
+
class Centurion::DockerViaApi
|
9
|
+
def initialize(hostname, port, tls_args = {}, api_version = nil)
|
10
|
+
@tls_args = default_tls_args(tls_args[:tls]).merge(tls_args.reject { |k, v| v.nil? }) # Required by tls_enable?
|
11
|
+
@base_uri = "http#{'s' if tls_enable?}://#{hostname}:#{port}"
|
12
|
+
api_version ||= "/v1.12"
|
13
|
+
@docker_api_version = api_version
|
14
|
+
configure_excon_globally
|
15
|
+
end
|
16
|
+
|
17
|
+
def ps(options={})
|
18
|
+
path = @docker_api_version + "/containers/json"
|
19
|
+
path += "?all=1" if options[:all]
|
20
|
+
response = Excon.get(@base_uri + path, tls_excon_arguments)
|
21
|
+
|
22
|
+
raise unless response.status == 200
|
23
|
+
JSON.load(response.body)
|
24
|
+
end
|
25
|
+
|
26
|
+
def inspect_image(image, tag = "latest")
|
27
|
+
repository = "#{image}:#{tag}"
|
28
|
+
path = @docker_api_version + "/images/#{repository}/json"
|
29
|
+
|
30
|
+
response = Excon.get(
|
31
|
+
@base_uri + path,
|
32
|
+
tls_excon_arguments.merge(headers: {'Accept' => 'application/json'})
|
33
|
+
)
|
34
|
+
raise response.inspect unless response.status == 200
|
35
|
+
JSON.load(response.body)
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_container(container_id)
|
39
|
+
path = @docker_api_version + "/containers/#{container_id}"
|
40
|
+
response = Excon.delete(
|
41
|
+
@base_uri + path,
|
42
|
+
tls_excon_arguments
|
43
|
+
)
|
44
|
+
raise response.inspect unless response.status == 204
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def stop_container(container_id, timeout = 30)
|
49
|
+
path = @docker_api_version + "/containers/#{container_id}/stop?t=#{timeout}"
|
50
|
+
response = Excon.post(
|
51
|
+
@base_uri + path,
|
52
|
+
tls_excon_arguments
|
53
|
+
)
|
54
|
+
raise response.inspect unless response.status == 204
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_container(configuration, name = nil)
|
59
|
+
path = @docker_api_version + "/containers/create"
|
60
|
+
response = Excon.post(
|
61
|
+
@base_uri + path,
|
62
|
+
tls_excon_arguments.merge(
|
63
|
+
query: name ? {name: "#{name}-#{SecureRandom.hex(7)}"} : nil,
|
64
|
+
body: configuration.to_json,
|
65
|
+
headers: { "Content-Type" => "application/json" }
|
66
|
+
)
|
67
|
+
)
|
68
|
+
raise response.inspect unless response.status == 201
|
69
|
+
JSON.load(response.body)
|
70
|
+
end
|
71
|
+
|
72
|
+
def start_container(container_id, configuration)
|
73
|
+
path = @docker_api_version + "/containers/#{container_id}/start"
|
74
|
+
response = Excon.post(
|
75
|
+
@base_uri + path,
|
76
|
+
tls_excon_arguments.merge(
|
77
|
+
body: configuration.to_json,
|
78
|
+
headers: { "Content-Type" => "application/json" }
|
79
|
+
)
|
80
|
+
)
|
81
|
+
case response.status
|
82
|
+
when 204
|
83
|
+
true
|
84
|
+
when 500
|
85
|
+
fail "Failed to start container! \"#{response.body}\""
|
86
|
+
else
|
87
|
+
raise response.inspect
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def restart_container(container_id, timeout = 30)
|
92
|
+
path = @docker_api_version + "/containers/#{container_id}/restart?t=#{timeout}"
|
93
|
+
response = Excon.post(
|
94
|
+
@base_uri + path,
|
95
|
+
tls_excon_arguments
|
96
|
+
)
|
97
|
+
case response.status
|
98
|
+
when 204
|
99
|
+
true
|
100
|
+
when 404
|
101
|
+
fail "Failed to start missing container! \"#{response.body}\""
|
102
|
+
when 500
|
103
|
+
fail "Failed to start existing container! \"#{response.body}\""
|
104
|
+
else
|
105
|
+
raise response.inspect
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def inspect_container(container_id)
|
110
|
+
path = @docker_api_version + "/containers/#{container_id}/json"
|
111
|
+
response = Excon.get(
|
112
|
+
@base_uri + path,
|
113
|
+
tls_excon_arguments
|
114
|
+
)
|
115
|
+
raise response.inspect unless response.status == 200
|
116
|
+
JSON.load(response.body)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# use on result of inspect container, not on an item in a list
|
122
|
+
def container_listening_on_port?(container, port)
|
123
|
+
port_bindings = container['HostConfig']['PortBindings']
|
124
|
+
return false unless port_bindings
|
125
|
+
|
126
|
+
port_bindings.values.flatten.compact.any? do |port_binding|
|
127
|
+
port_binding['HostPort'].to_i == port.to_i
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def tls_enable?
|
132
|
+
@tls_args.is_a?(Hash) && @tls_args.size > 0
|
133
|
+
end
|
134
|
+
|
135
|
+
def tls_excon_arguments
|
136
|
+
return {} unless [:tlscert, :tlskey].all? { |key| @tls_args.key?(key) }
|
137
|
+
|
138
|
+
{
|
139
|
+
client_cert: @tls_args[:tlscert],
|
140
|
+
client_key: @tls_args[:tlskey]
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
def configure_excon_globally
|
145
|
+
Excon.defaults[:connect_timeout] = 120
|
146
|
+
Excon.defaults[:read_timeout] = 120
|
147
|
+
Excon.defaults[:write_timeout] = 120
|
148
|
+
Excon.defaults[:debug_request] = true
|
149
|
+
Excon.defaults[:debug_response] = true
|
150
|
+
Excon.defaults[:nonblock] = false
|
151
|
+
Excon.defaults[:tcp_nodelay] = true
|
152
|
+
Excon.defaults[:ssl_ca_file] = @tls_args[:tlscacert]
|
153
|
+
end
|
154
|
+
|
155
|
+
def default_tls_args(tls_enabled)
|
156
|
+
if tls_enabled
|
157
|
+
{
|
158
|
+
tlscacert: File.expand_path('~/.docker/ca.pem'),
|
159
|
+
tlscert: File.expand_path('~/.docker/cert.pem'),
|
160
|
+
tlskey: File.expand_path('~/.docker/key.pem')
|
161
|
+
}
|
162
|
+
else
|
163
|
+
{}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pty'
|
2
|
+
require_relative 'logging'
|
3
|
+
require_relative 'shell'
|
4
|
+
|
5
|
+
module Centurion; end
|
6
|
+
|
7
|
+
class Centurion::DockerViaCli
|
8
|
+
include Centurion::Logging
|
9
|
+
|
10
|
+
def initialize(hostname, port, docker_path, tls_args = {})
|
11
|
+
@docker_host = "tcp://#{hostname}:#{port}"
|
12
|
+
@docker_path = docker_path
|
13
|
+
@tls_args = tls_args
|
14
|
+
end
|
15
|
+
|
16
|
+
def pull(image, tag='latest')
|
17
|
+
info 'Using CLI to pull'
|
18
|
+
Centurion::Shell.echo(build_command(:pull, "#{image}:#{tag}"))
|
19
|
+
end
|
20
|
+
|
21
|
+
def tail(container_id)
|
22
|
+
info "Tailing the logs on #{container_id}"
|
23
|
+
Centurion::Shell.echo(build_command(:logs, container_id))
|
24
|
+
end
|
25
|
+
|
26
|
+
def attach(container_id)
|
27
|
+
Centurion::Shell.echo(build_command(:attach, container_id))
|
28
|
+
end
|
29
|
+
|
30
|
+
def exec(container_id, commandline)
|
31
|
+
Centurion::Shell.echo(build_command(:exec, "#{container_id} #{commandline}"))
|
32
|
+
end
|
33
|
+
|
34
|
+
def exec_it(container_id, commandline)
|
35
|
+
# the "or true" on the command is to prevent an exception from Shell.validate_status
|
36
|
+
# because docker exec returns the same exit code as the latest command executed on
|
37
|
+
# the shell, which causes an exception to be raised if the latest comand executed
|
38
|
+
# was unsuccessful when you exit the shell.
|
39
|
+
Centurion::Shell.echo(build_command(:exec, "-it #{container_id} #{commandline} || true"))
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def self.tls_keys
|
45
|
+
[:tlscacert, :tlscert, :tlskey]
|
46
|
+
end
|
47
|
+
|
48
|
+
def all_tls_path_available?
|
49
|
+
self.class.tls_keys.all? { |key| @tls_args.key?(key) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def tls_parameters
|
53
|
+
return '' if @tls_args.nil? || @tls_args.empty?
|
54
|
+
|
55
|
+
tls_flags = ''
|
56
|
+
|
57
|
+
# --tlsverify can be set without passing the cacert, cert and key flags
|
58
|
+
if @tls_args[:tls] == true || all_tls_path_available?
|
59
|
+
tls_flags << ' --tlsverify'
|
60
|
+
end
|
61
|
+
|
62
|
+
self.class.tls_keys.each do |key|
|
63
|
+
tls_flags << " --#{key}=#{@tls_args[key]}" if @tls_args[key]
|
64
|
+
end
|
65
|
+
|
66
|
+
tls_flags
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_command(action, destination)
|
70
|
+
command = "#{@docker_path} -H=#{@docker_host}"
|
71
|
+
command << tls_parameters
|
72
|
+
command << case action
|
73
|
+
when :pull then ' pull '
|
74
|
+
when :logs then ' logs -f '
|
75
|
+
when :attach then ' attach '
|
76
|
+
when :exec then ' exec '
|
77
|
+
end
|
78
|
+
command << destination
|
79
|
+
command
|
80
|
+
end
|
81
|
+
end
|